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