File indexing completed on 2024-05-19 05:08:15
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-FileCopyrightText: 2020 Robert Szczesiak <dev.rszczesiak@gmail.com> 0010 SPDX-License-Identifier: GPL-2.0-or-later 0011 */ 0012 0013 #include "kcategoriesview_p.h" 0014 0015 #include <typeinfo> 0016 0017 // ---------------------------------------------------------------------------- 0018 // QT Includes 0019 0020 #include <QBitArray> 0021 #include <QMenu> 0022 0023 // ---------------------------------------------------------------------------- 0024 // KDE Includes 0025 0026 #include <KMessageBox> 0027 0028 // ---------------------------------------------------------------------------- 0029 // Project Includes 0030 0031 #include "mymoneyfile.h" 0032 #include "mymoneyexception.h" 0033 #include "kmymoneysettings.h" 0034 #include "knewaccountdlg.h" 0035 #include "kcategoryreassigndlg.h" 0036 #include "mymoneyschedule.h" 0037 #include "mymoneybudget.h" 0038 #include "mymoneytransaction.h" 0039 #include "mymoneytransactionfilter.h" 0040 #include "mymoneyenums.h" 0041 #include "storageenums.h" 0042 #include "menuenums.h" 0043 #include "mymoneymoney.h" 0044 #include "accountdelegate.h" 0045 0046 using namespace Icons; 0047 0048 KCategoriesView::KCategoriesView(QWidget *parent) : 0049 KMyMoneyViewBase(*new KCategoriesViewPrivate(this), parent) 0050 { 0051 Q_D(KCategoriesView); 0052 d->init(); 0053 0054 connect(pActions[eMenu::Action::NewCategory], &QAction::triggered, this, &KCategoriesView::slotNewCategory); 0055 connect(pActions[eMenu::Action::EditCategory], &QAction::triggered, this, &KCategoriesView::slotEditCategory); 0056 connect(pActions[eMenu::Action::DeleteCategory], &QAction::triggered, this, &KCategoriesView::slotDeleteCategory); 0057 0058 d->ui->m_accountTree->setItemDelegate(new AccountDelegate(d->ui->m_accountTree)); 0059 connect(MyMoneyFile::instance()->accountsModel(), &AccountsModel::profitLossChanged, this, &KCategoriesView::slotProfitLossChanged); 0060 0061 d->m_sharedToolbarActions.insert(eMenu::Action::FileNew, pActions[eMenu::Action::NewCategory]); 0062 0063 d->ui->m_accountTree->setSelectionMode(QAbstractItemView::SingleSelection); 0064 d->ui->m_accountTree->setDragEnabled(true); 0065 d->ui->m_accountTree->setAcceptDrops(true); 0066 d->ui->m_accountTree->setDropIndicatorShown(true); 0067 d->ui->m_accountTree->setDragDropMode(QAbstractItemView::InternalMove); 0068 } 0069 0070 KCategoriesView::~KCategoriesView() 0071 { 0072 } 0073 0074 void KCategoriesView::slotSettingsChanged() 0075 { 0076 Q_D(KCategoriesView); 0077 d->m_proxyModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts()); 0078 d->m_proxyModel->setHideEquityAccounts(true); 0079 d->m_proxyModel->setHideZeroBalancedEquityAccounts(true); 0080 d->m_proxyModel->setShowAllEntries(KMyMoneySettings::showAllAccounts()); 0081 d->m_proxyModel->setHideFavoriteAccounts(true); 0082 0083 MyMoneyFile::instance()->accountsModel()->setColorScheme(AccountsModel::Positive, KMyMoneySettings::schemeColor(SchemeColor::Positive)); 0084 MyMoneyFile::instance()->accountsModel()->setColorScheme(AccountsModel::Negative, KMyMoneySettings::schemeColor(SchemeColor::Negative)); 0085 } 0086 0087 0088 void KCategoriesView::updateActions(const SelectedObjects& selections) 0089 { 0090 Q_D(KCategoriesView); 0091 const auto file = MyMoneyFile::instance(); 0092 0093 // check if there is anything todo and quit if not 0094 if (selections.selection(SelectedObjects::Account).count() < 1 0095 && d->m_currentCategory.id().isEmpty() ) { 0096 return; 0097 } 0098 0099 pActions[eMenu::Action::NewCategory]->setEnabled(true); 0100 pActions[eMenu::Action::EditCategory]->setEnabled(false); 0101 pActions[eMenu::Action::DeleteCategory]->setEnabled(false); 0102 0103 const auto accountIds = selections.selection(SelectedObjects::Account); 0104 if (accountIds.isEmpty()) { 0105 d->m_currentCategory = MyMoneyAccount(); 0106 return; 0107 } 0108 const auto acc = file->accountsModel()->itemById(accountIds.at(0)); 0109 auto b = file->isStandardAccount(acc.id()) ? false : true; 0110 0111 QBitArray skip((int)eStorage::Reference::Count); 0112 switch (acc.accountType()) { 0113 case eMyMoney::Account::Type::Income: 0114 case eMyMoney::Account::Type::Expense: 0115 d->m_currentCategory = acc; 0116 pActions[eMenu::Action::EditCategory]->setEnabled(b); 0117 0118 // enable delete action, if category/account itself is not referenced 0119 // by any object except accounts, because we want to allow 0120 // deleting of sub-categories. Also, we allow transactions, schedules and budgets 0121 // to be present because we can re-assign them during the delete process 0122 skip.fill(false); 0123 skip.setBit((int)eStorage::Reference::Transaction); 0124 skip.setBit((int)eStorage::Reference::Account); 0125 skip.setBit((int)eStorage::Reference::Schedule); 0126 skip.setBit((int)eStorage::Reference::Budget); 0127 pActions[eMenu::Action::DeleteCategory]->setEnabled(b && !file->isReferenced(acc, skip)); 0128 break; 0129 0130 default: 0131 d->m_currentCategory = MyMoneyAccount(); 0132 break; 0133 } 0134 } 0135 0136 /** 0137 * The view is notified that an unused income expense account has been hidden. 0138 */ 0139 void KCategoriesView::slotUnusedIncomeExpenseAccountHidden() 0140 { 0141 Q_D(KCategoriesView); 0142 d->m_haveUnusedCategories = true; 0143 d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories); 0144 } 0145 0146 void KCategoriesView::slotProfitLossChanged(const MyMoneyMoney &profit, bool isApproximate) 0147 { 0148 Q_D(KCategoriesView); 0149 const auto formattedValue = profit.isNegative() ? d->formatViewLabelValue(-profit, KMyMoneySettings::schemeColor(SchemeColor::Negative)) 0150 : d->formatViewLabelValue(profit, KMyMoneySettings::schemeColor(SchemeColor::Positive)); 0151 if (profit.isNegative()) 0152 d->updateViewLabel(d->ui->m_totalProfitsLabel, 0153 isApproximate ? i18nc("Approximate loss", "Loss: ~%1", formattedValue) 0154 : i18n("Loss: %1", formattedValue)); 0155 else 0156 d->updateViewLabel(d->ui->m_totalProfitsLabel, 0157 isApproximate ? i18nc("Approximate profit", "Profit: ~%1", formattedValue) 0158 : i18n("Profit: %1", formattedValue)); 0159 } 0160 0161 void KCategoriesView::slotNewCategory() 0162 { 0163 Q_D(KCategoriesView); 0164 MyMoneyAccount parent; 0165 MyMoneyAccount account; 0166 0167 // Preselect the parent account by looking at the current selected account/category 0168 if (!d->m_currentCategory.id().isEmpty() && 0169 d->m_currentCategory.isIncomeExpense()) { 0170 try { 0171 parent = MyMoneyFile::instance()->account(d->m_currentCategory.id()); 0172 } catch (const MyMoneyException &) { 0173 } 0174 } 0175 0176 KNewAccountDlg::createCategory(account, parent); 0177 } 0178 0179 void KCategoriesView::slotEditCategory() 0180 { 0181 Q_D(KCategoriesView); 0182 if (d->m_currentCategory.id().isEmpty()) 0183 return; 0184 0185 const auto file = MyMoneyFile::instance(); 0186 if (file->isStandardAccount(d->m_currentCategory.id())) 0187 return; 0188 0189 switch (d->m_currentCategory.accountType()) { 0190 case eMyMoney::Account::Type::Income: 0191 case eMyMoney::Account::Type::Expense: 0192 break; 0193 default: 0194 return; 0195 } 0196 0197 QPointer<KNewAccountDlg> dlg = 0198 new KNewAccountDlg(d->m_currentCategory, true, true, 0, i18n("Edit category '%1'", d->m_currentCategory.name())); 0199 0200 dlg->setOpeningBalanceShown(false); 0201 dlg->setOpeningDateShown(false); 0202 0203 if (dlg->exec() == QDialog::Accepted) { 0204 if (dlg != nullptr) { 0205 try { 0206 MyMoneyFileTransaction ft(i18nc("Undo action description", "Edit category"), false); 0207 0208 auto account = dlg->account(); 0209 auto parent = dlg->parentAccount(); 0210 0211 // we need to modify first, as reparent would override all other changes 0212 file->modifyAccount(account); 0213 if (account.parentAccountId() != parent.id()) 0214 file->reparentAccount(account, parent); 0215 0216 ft.commit(); 0217 // update our local copy of the category data 0218 d->m_currentCategory = account; 0219 } catch (const MyMoneyException& e) { 0220 KMessageBox::error(this, i18n("Unable to modify category '%1'. Cause: %2", d->m_currentCategory.name(), QString::fromLatin1(e.what()))); 0221 } 0222 } 0223 } 0224 0225 delete dlg; 0226 } 0227 0228 void KCategoriesView::slotDeleteCategory() 0229 { 0230 Q_D(KCategoriesView); 0231 if (d->m_currentCategory.id().isEmpty()) 0232 return; // need an account ID 0233 0234 const auto file = MyMoneyFile::instance(); 0235 // can't delete standard accounts or account which still have transactions assigned 0236 if (file->isStandardAccount(d->m_currentCategory.id())) 0237 return; 0238 0239 // check if the account is referenced by a transaction or schedule 0240 QBitArray skip((int)eStorage::Reference::Count); 0241 skip.fill(false); 0242 skip.setBit((int)eStorage::Reference::Account); 0243 skip.setBit((int)eStorage::Reference::Institution); 0244 skip.setBit((int)eStorage::Reference::Payee); 0245 skip.setBit((int)eStorage::Reference::Tag); 0246 skip.setBit((int)eStorage::Reference::Security); 0247 skip.setBit((int)eStorage::Reference::Currency); 0248 skip.setBit((int)eStorage::Reference::Price); 0249 const auto hasReference = file->isReferenced(d->m_currentCategory, skip); 0250 0251 // if we get here and still have transactions referencing the account, we 0252 // need to check with the user to possibly re-assign them to a different account 0253 auto needAskUser = true; 0254 0255 MyMoneyFileTransaction ft; 0256 0257 if (hasReference) { 0258 // show transaction reassignment dialog 0259 0260 needAskUser = false; 0261 auto dlg = new KCategoryReassignDlg(this); 0262 auto categoryId = dlg->show(d->m_currentCategory); 0263 delete dlg; // and kill the dialog 0264 if (categoryId.isEmpty()) 0265 return; // the user aborted the dialog, so let's abort as well 0266 0267 auto newCategory = file->account(categoryId); 0268 try { 0269 { 0270 /* 0271 d->m_currentCategory.id() is the old id, categoryId the new one 0272 Now search all transactions and schedules that reference d->m_currentCategory.id() 0273 and replace that with categoryId. 0274 */ 0275 // get the list of all transactions that reference the old account 0276 MyMoneyTransactionFilter filter(d->m_currentCategory.id()); 0277 filter.setReportAllSplits(false); 0278 QList<MyMoneyTransaction> tlist; 0279 QList<MyMoneyTransaction>::iterator it_t; 0280 file->transactionList(tlist, filter); 0281 0282 for (it_t = tlist.begin(); it_t != tlist.end(); ++it_t) { 0283 MyMoneyTransaction t = (*it_t); 0284 if (t.replaceId(categoryId, d->m_currentCategory.id())) 0285 file->modifyTransaction(t); 0286 } 0287 } 0288 // now fix all schedules 0289 { 0290 QList<MyMoneySchedule> slist = file->scheduleList(d->m_currentCategory.id()); 0291 QList<MyMoneySchedule>::iterator it_s; 0292 0293 for (it_s = slist.begin(); it_s != slist.end(); ++it_s) { 0294 MyMoneySchedule sch = (*it_s); 0295 if (sch.replaceId(categoryId, d->m_currentCategory.id())) { 0296 file->modifySchedule(sch); 0297 } 0298 } 0299 } 0300 // now fix all budgets 0301 { 0302 QList<MyMoneyBudget> blist = file->budgetList(); 0303 QList<MyMoneyBudget>::const_iterator it_b; 0304 for (it_b = blist.constBegin(); it_b != blist.constEnd(); ++it_b) { 0305 if ((*it_b).hasReferenceTo(d->m_currentCategory.id())) { 0306 MyMoneyBudget b = (*it_b); 0307 MyMoneyBudget::AccountGroup fromBudget = b.account(d->m_currentCategory.id()); 0308 MyMoneyBudget::AccountGroup toBudget = b.account(categoryId); 0309 toBudget += fromBudget; 0310 b.setAccount(toBudget, categoryId); 0311 b.removeReference(d->m_currentCategory.id()); 0312 file->modifyBudget(b); 0313 } 0314 } 0315 } 0316 } catch (MyMoneyException &e) { 0317 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()))); 0318 return; 0319 } 0320 } 0321 0322 // retain the account name for a possible later usage in the error message box 0323 // since the account removal notifies the views the selected account can be changed 0324 // so we make sure by doing this that we display the correct name in the error message 0325 auto selectedAccountName = d->m_currentCategory.name(); 0326 0327 // at this point, we must not have a reference to the account 0328 // to be deleted anymore 0329 // special handling for categories to allow deleting of empty subcategories 0330 { 0331 // open a compound statement here to be able to declare variables 0332 // which would otherwise not work within a case label. 0333 0334 // case A - only a single, unused category without subcats selected 0335 if (d->m_currentCategory.accountList().isEmpty()) { 0336 if (!needAskUser 0337 || (KMessageBox::questionTwoActions(this, 0338 i18n("<qt>Do you really want to delete category <b>%1</b>?</qt>", selectedAccountName), 0339 i18nc("@title:window", "Delete category"), 0340 KMMYesNo::yes(), 0341 KMMYesNo::no()) 0342 == KMessageBox::PrimaryAction)) { 0343 try { 0344 file->removeAccount(d->m_currentCategory); 0345 d->m_currentCategory.clearId(); 0346 ft.commit(); 0347 } catch (const MyMoneyException &e) { 0348 KMessageBox::error(this, i18n("<qt>Unable to delete category <b>%1</b>. Cause: %2</qt>", selectedAccountName, QString::fromLatin1(e.what()))); 0349 } 0350 } 0351 return; 0352 } 0353 // case B - we have some subcategories, maybe the user does not want to 0354 // delete them all, but just the category itself? 0355 auto parentAccount = file->account(d->m_currentCategory.parentAccountId()); 0356 0357 QStringList accountsToReparent; 0358 int result = KMessageBox::questionTwoActionsCancel(this, 0359 i18n("<qt>Do you want to delete category <b>%1</b> with all its sub-categories or only " 0360 "the category itself? If you only delete the category itself, all its sub-categories " 0361 "will be made sub-categories of <b>%2</b>.</qt>", 0362 selectedAccountName, 0363 parentAccount.name()), 0364 QString(), 0365 KGuiItem(i18n("Delete all")), 0366 KGuiItem(i18n("Just the category"))); 0367 if (result == KMessageBox::Cancel) 0368 return; // cancel pressed? ok, no delete then... 0369 // "No" means "Just the category" and that means we need to reparent all subaccounts 0370 bool need_confirmation = false; 0371 // case C - User only wants to delete the category itself 0372 if (result == KMessageBox::SecondaryAction) 0373 accountsToReparent = d->m_currentCategory.accountList(); 0374 else { 0375 // case D - User wants to delete all subcategories, now check all subcats of 0376 // d->m_currentCategory and remember all that cannot be deleted and 0377 // must be "reparented" 0378 const auto subAccountList = d->m_currentCategory.accountList(); 0379 for (const auto& accountID : qAsConst(subAccountList)) { 0380 // reparent account if a transaction is assigned 0381 if (file->transactionCount(accountID) != 0) 0382 accountsToReparent.push_back(accountID); 0383 else if (!file->account(accountID).accountList().isEmpty()) { 0384 // or if we have at least one sub-account that is used for transactions 0385 if (!file->hasOnlyUnusedAccounts(file->account(accountID).accountList())) { 0386 accountsToReparent.push_back(accountID); 0387 //qDebug() << "subaccount not empty"; 0388 } 0389 } 0390 } 0391 if (!accountsToReparent.isEmpty()) 0392 need_confirmation = true; 0393 } 0394 if (!accountsToReparent.isEmpty() && need_confirmation) { 0395 if (KMessageBox::questionTwoActions(this, 0396 i18n("<p>Some sub-categories of category <b>%1</b> cannot " 0397 "be deleted, because they are still used. They will be made sub-categories of <b>%2</b>. Proceed?</p>", 0398 selectedAccountName, 0399 parentAccount.name()), 0400 i18nc("@title:window", "Delete category"), 0401 KMMYesNo::yes(), 0402 KMMYesNo::no()) 0403 != KMessageBox::PrimaryAction) { 0404 return; // user gets wet feet... 0405 } 0406 } 0407 // all good, now first reparent selected sub-categories 0408 try { 0409 auto parent = file->account(d->m_currentCategory.parentAccountId()); 0410 for (QStringList::const_iterator it = accountsToReparent.constBegin(); it != accountsToReparent.constEnd(); ++it) { 0411 auto child = file->account(*it); 0412 file->reparentAccount(child, parent); 0413 } 0414 // reload the account because the sub-account list might have changed 0415 d->m_currentCategory = file->account(d->m_currentCategory.id()); 0416 // now recursively delete remaining sub-categories 0417 file->removeAccountList(d->m_currentCategory.accountList()); 0418 // don't forget to update d->m_currentCategory, because we still have a copy of 0419 // the old account list, which is no longer valid 0420 d->m_currentCategory = file->account(d->m_currentCategory.id()); 0421 } catch (const MyMoneyException &e) { 0422 KMessageBox::error(this, i18n("<qt>Unable to delete a sub-category of category <b>%1</b>. Reason: %2</qt>", selectedAccountName, QString::fromLatin1(e.what()))); 0423 return; 0424 } 0425 } 0426 // the category/account is deleted after the switch 0427 try { 0428 file->removeAccount(d->m_currentCategory); 0429 d->m_currentCategory.clearId(); 0430 ft.commit(); 0431 } catch (const MyMoneyException &e) { 0432 KMessageBox::error(this, i18n("Unable to delete category '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what()))); 0433 } 0434 }