diff --git a/kmymoney/views/kcategoriesview.cpp b/kmymoney/views/kcategoriesview.cpp index 2cf7fe88e..332c2523b 100644 --- a/kmymoney/views/kcategoriesview.cpp +++ b/kmymoney/views/kcategoriesview.cpp @@ -1,498 +1,494 @@ /*************************************************************************** kcategoriesview.cpp - description ------------------- begin : Sun Jan 20 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kcategoriesview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "knewaccountdlg.h" #include "kcategoryreassigndlg.h" #include "mymoneyschedule.h" #include "mymoneybudget.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyenums.h" #include "storageenums.h" #include "menuenums.h" using namespace Icons; KCategoriesView::KCategoriesView(QWidget *parent) : KMyMoneyAccountsViewBase(*new KCategoriesViewPrivate(this), parent) { connect(pActions[eMenu::Action::NewCategory], &QAction::triggered, this, &KCategoriesView::slotNewCategory); connect(pActions[eMenu::Action::EditCategory], &QAction::triggered, this, &KCategoriesView::slotEditCategory); connect(pActions[eMenu::Action::DeleteCategory], &QAction::triggered, this, &KCategoriesView::slotDeleteCategory); } KCategoriesView::~KCategoriesView() { } void KCategoriesView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KCategoriesView); QTimer::singleShot(0, d->ui->m_accountTree, SLOT(setFocus())); } break; default: break; } } void KCategoriesView::refresh() { Q_D(KCategoriesView); if (!isVisible()) { d->m_needsRefresh = true; return; } d->m_needsRefresh = false; d->m_proxyModel->invalidate(); d->m_proxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts()); // reinitialize the default state of the hidden categories label d->m_haveUnusedCategories = false; d->ui->m_hiddenCategories->hide(); d->m_proxyModel->setHideUnusedIncomeExpenseAccounts(KMyMoneySettings::hideUnusedCategory()); } void KCategoriesView::showEvent(QShowEvent * event) { Q_D(KCategoriesView); if (!d->m_proxyModel) d->init(); emit customActionRequested(View::Categories, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); // don't forget base class implementation QWidget::showEvent(event); } void KCategoriesView::updateActions(const MyMoneyObject& obj) { Q_D(KCategoriesView); if (typeid(obj) != typeid(MyMoneyAccount) && (obj.id().isEmpty() && d->m_currentCategory.id().isEmpty())) // do not disable actions that were already disabled)) return; const auto& acc = static_cast(obj); if (d->m_currentCategory.id().isEmpty() && acc.id().isEmpty()) return; switch (acc.accountType()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: { const auto file = MyMoneyFile::instance(); auto b = file->isStandardAccount(acc.id()) ? false : true; - pActions[eMenu::Action::NewCategory]->setEnabled(true); pActions[eMenu::Action::EditCategory]->setEnabled(b); // enable delete action, if category/account itself is not referenced // by any object except accounts, because we want to allow // deleting of sub-categories. Also, we allow transactions, schedules and budgets // to be present because we can re-assign them during the delete process QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); skip.setBit((int)eStorage::Reference::Transaction); skip.setBit((int)eStorage::Reference::Account); skip.setBit((int)eStorage::Reference::Schedule); skip.setBit((int)eStorage::Reference::Budget); pActions[eMenu::Action::DeleteCategory]->setEnabled(b && !file->isReferenced(acc, skip)); d->m_currentCategory = acc; break; } default: - { - pActions[eMenu::Action::NewCategory]->setEnabled(false); pActions[eMenu::Action::EditCategory]->setEnabled(false); pActions[eMenu::Action::DeleteCategory]->setEnabled(false); d->m_currentCategory = MyMoneyAccount(); break; - } } } /** * The view is notified that an unused income expense account has been hidden. */ void KCategoriesView::slotUnusedIncomeExpenseAccountHidden() { Q_D(KCategoriesView); d->m_haveUnusedCategories = true; d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories); } void KCategoriesView::slotProfitChanged(const MyMoneyMoney &profit) { Q_D(KCategoriesView); d->netBalProChanged(profit, d->ui->m_totalProfitsLabel, View::Categories); } void KCategoriesView::slotShowCategoriesMenu(const MyMoneyAccount& acc) { Q_UNUSED(acc); pMenus[eMenu::Menu::Category]->exec(QCursor::pos()); } void KCategoriesView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenContextMenu: slotShowCategoriesMenu(static_cast(obj)); break; default: break; } } void KCategoriesView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { Q_D(KCategoriesView); switch (intent) { case eView::Intent::UpdateProfit: if (variant.count() == 1 && d->m_proxyModel) slotProfitChanged(variant.first().value()); break; default: break; } } void KCategoriesView::slotNewCategory() { Q_D(KCategoriesView); MyMoneyAccount parent; MyMoneyAccount account; // Preselect the parent account by looking at the current selected account/category if (!d->m_currentCategory.id().isEmpty() && d->m_currentCategory.isIncomeExpense()) { try { parent = MyMoneyFile::instance()->account(d->m_currentCategory.id()); } catch (const MyMoneyException &) { } } KNewAccountDlg::createCategory(account, parent); } void KCategoriesView::slotEditCategory() { Q_D(KCategoriesView); if (d->m_currentCategory.id().isEmpty()) return; const auto file = MyMoneyFile::instance(); if (file->isStandardAccount(d->m_currentCategory.id())) return; switch (d->m_currentCategory.accountType()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: break; default: return; } // set a status message so that the application can't be closed until the editing is done // slotStatusMsg(caption); QPointer dlg = new KNewAccountDlg(d->m_currentCategory, true, true, 0, i18n("Edit category '%1'", d->m_currentCategory.name())); dlg->setOpeningBalanceShown(false); dlg->setOpeningDateShown(false); if (dlg && dlg->exec() == QDialog::Accepted) { try { MyMoneyFileTransaction ft; auto account = dlg->account(); auto parent = dlg->parentAccount(); // we need to modify first, as reparent would override all other changes file->modifyAccount(account); if (account.parentAccountId() != parent.id()) file->reparentAccount(account, parent); ft.commit(); // reload the account object as it might have changed in the meantime emit selectByObject(account, eView::Intent::None); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to modify category '%1'. Cause: %2", d->m_currentCategory.name(), QString::fromLatin1(e.what()))); } } delete dlg; // ready(); } void KCategoriesView::slotDeleteCategory() { Q_D(KCategoriesView); if (d->m_currentCategory.id().isEmpty()) return; // need an account ID const auto file = MyMoneyFile::instance(); // can't delete standard accounts or account which still have transactions assigned if (file->isStandardAccount(d->m_currentCategory.id())) return; // check if the account is referenced by a transaction or schedule QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); skip.setBit((int)eStorage::Reference::Account); skip.setBit((int)eStorage::Reference::Institution); skip.setBit((int)eStorage::Reference::Payee); skip.setBit((int)eStorage::Reference::Tag); skip.setBit((int)eStorage::Reference::Security); skip.setBit((int)eStorage::Reference::Currency); skip.setBit((int)eStorage::Reference::Price); const auto hasReference = file->isReferenced(d->m_currentCategory, skip); // if we get here and still have transactions referencing the account, we // need to check with the user to possibly re-assign them to a different account auto needAskUser = true; MyMoneyFileTransaction ft; if (hasReference) { // show transaction reassignment dialog needAskUser = false; auto dlg = new KCategoryReassignDlg(this); auto categoryId = dlg->show(d->m_currentCategory); delete dlg; // and kill the dialog if (categoryId.isEmpty()) return; // the user aborted the dialog, so let's abort as well auto newCategory = file->account(categoryId); try { { // KMSTATUS(i18n("Adjusting transactions...")); /* d->m_currentCategory.id() is the old id, categoryId the new one Now search all transactions and schedules that reference d->m_currentCategory.id() and replace that with categoryId. */ // get the list of all transactions that reference the old account MyMoneyTransactionFilter filter(d->m_currentCategory.id()); filter.setReportAllSplits(false); QList tlist; QList::iterator it_t; file->transactionList(tlist, filter); // slotStatusProgressBar(0, tlist.count()); // int cnt = 0; for (it_t = tlist.begin(); it_t != tlist.end(); ++it_t) { // slotStatusProgressBar(++cnt, 0); MyMoneyTransaction t = (*it_t); if (t.replaceId(categoryId, d->m_currentCategory.id())) file->modifyTransaction(t); } // slotStatusProgressBar(tlist.count(), 0); } // now fix all schedules { // KMSTATUS(i18n("Adjusting scheduled transactions...")); QList slist = file->scheduleList(d->m_currentCategory.id()); QList::iterator it_s; // int cnt = 0; // slotStatusProgressBar(0, slist.count()); for (it_s = slist.begin(); it_s != slist.end(); ++it_s) { // slotStatusProgressBar(++cnt, 0); MyMoneySchedule sch = (*it_s); if (sch.replaceId(categoryId, d->m_currentCategory.id())) { file->modifySchedule(sch); } } // slotStatusProgressBar(slist.count(), 0); } // now fix all budgets { // KMSTATUS(i18n("Adjusting budgets...")); QList blist = file->budgetList(); QList::const_iterator it_b; for (it_b = blist.constBegin(); it_b != blist.constEnd(); ++it_b) { if ((*it_b).hasReferenceTo(d->m_currentCategory.id())) { MyMoneyBudget b = (*it_b); MyMoneyBudget::AccountGroup fromBudget = b.account(d->m_currentCategory.id()); MyMoneyBudget::AccountGroup toBudget = b.account(categoryId); toBudget += fromBudget; b.setAccount(toBudget, categoryId); b.removeReference(d->m_currentCategory.id()); file->modifyBudget(b); } } // slotStatusProgressBar(blist.count(), 0); } } catch (MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to exchange category %1 with category %2. Reason: %3", d->m_currentCategory.name(), newCategory.name(), QString::fromLatin1(e.what()))); // slotStatusProgressBar(-1, -1); return; } // slotStatusProgressBar(-1, -1); } // retain the account name for a possible later usage in the error message box // since the account removal notifies the views the selected account can be changed // so we make sure by doing this that we display the correct name in the error message auto selectedAccountName = d->m_currentCategory.name(); // at this point, we must not have a reference to the account // to be deleted anymore // special handling for categories to allow deleting of empty subcategories { // open a compound statement here to be able to declare variables // which would otherwise not work within a case label. // case A - only a single, unused category without subcats selected if (d->m_currentCategory.accountList().isEmpty()) { if (!needAskUser || (KMessageBox::questionYesNo(this, i18n("Do you really want to delete category %1?", selectedAccountName)) == KMessageBox::Yes)) { try { file->removeAccount(d->m_currentCategory); d->m_currentCategory.clearId(); emit selectByObject(d->m_currentCategory, eView::Intent::None); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete category %1. Cause: %2", selectedAccountName, QString::fromLatin1(e.what()))); } } return; } // case B - we have some subcategories, maybe the user does not want to // delete them all, but just the category itself? auto parentAccount = file->account(d->m_currentCategory.parentAccountId()); QStringList accountsToReparent; int result = KMessageBox::questionYesNoCancel(this, i18n("Do you want to delete category %1 with all its sub-categories or only " "the category itself? If you only delete the category itself, all its sub-categories " "will be made sub-categories of %2.", selectedAccountName, parentAccount.name()), QString(), KGuiItem(i18n("Delete all")), KGuiItem(i18n("Just the category"))); if (result == KMessageBox::Cancel) return; // cancel pressed? ok, no delete then... // "No" means "Just the category" and that means we need to reparent all subaccounts bool need_confirmation = false; // case C - User only wants to delete the category itself if (result == KMessageBox::No) accountsToReparent = d->m_currentCategory.accountList(); else { // case D - User wants to delete all subcategories, now check all subcats of // d->m_currentCategory and remember all that cannot be deleted and // must be "reparented" foreach (const auto accountID, d->m_currentCategory.accountList()) { // reparent account if a transaction is assigned if (file->transactionCount(accountID) != 0) accountsToReparent.push_back(accountID); else if (!file->account(accountID).accountList().isEmpty()) { // or if we have at least one sub-account that is used for transactions if (!file->hasOnlyUnusedAccounts(file->account(accountID).accountList())) { accountsToReparent.push_back(accountID); //qDebug() << "subaccount not empty"; } } } if (!accountsToReparent.isEmpty()) need_confirmation = true; } if (!accountsToReparent.isEmpty() && need_confirmation) { if (KMessageBox::questionYesNo(this, i18n("

Some sub-categories of category %1 cannot " "be deleted, because they are still used. They will be made sub-categories of %2. Proceed?

", selectedAccountName, parentAccount.name())) != KMessageBox::Yes) { return; // user gets wet feet... } } // all good, now first reparent selected sub-categories try { auto parent = file->account(d->m_currentCategory.parentAccountId()); for (QStringList::const_iterator it = accountsToReparent.constBegin(); it != accountsToReparent.constEnd(); ++it) { auto child = file->account(*it); file->reparentAccount(child, parent); } // reload the account because the sub-account list might have changed d->m_currentCategory = file->account(d->m_currentCategory.id()); // now recursively delete remaining sub-categories file->removeAccountList(d->m_currentCategory.accountList()); // don't forget to update d->m_currentCategory, because we still have a copy of // the old account list, which is no longer valid d->m_currentCategory = file->account(d->m_currentCategory.id()); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete a sub-category of category %1. Reason: %2", selectedAccountName, QString::fromLatin1(e.what()))); return; } } // the category/account is deleted after the switch try { file->removeAccount(d->m_currentCategory); d->m_currentCategory.clearId(); emit selectByObject(MyMoneyAccount(), eView::Intent::None); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete category '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what()))); } }