File indexing completed on 2024-05-19 05:08:20

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: 2000-2002 Andreas Nicolai <Andreas.Nicolai@gmx.net>
0009     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0010     SPDX-License-Identifier: GPL-2.0-or-later
0011 */
0012 
0013 #ifndef KPAYEESVIEW_P_H
0014 #define KPAYEESVIEW_P_H
0015 
0016 // ----------------------------------------------------------------------------
0017 // QT Includes
0018 
0019 #include <QDesktopServices>
0020 #include <QRegularExpression>
0021 
0022 // ----------------------------------------------------------------------------
0023 // KDE Includes
0024 
0025 #include <KLocalizedString>
0026 #include <KMessageBox>
0027 #include <KHelpClient>
0028 #include <KSharedConfig>
0029 
0030 // ----------------------------------------------------------------------------
0031 // Project Includes
0032 
0033 #include "config-kmymoney.h"
0034 
0035 #include "accountsmodel.h"
0036 #include "icons.h"
0037 #include "itemrenameproxymodel.h"
0038 #include "journalmodel.h"
0039 #include "kmymoneymvccombo.h"
0040 #include "kmymoneysettings.h"
0041 #include "kmymoneyutils.h"
0042 #include "kmymoneyviewbase_p.h"
0043 #include "kpayeereassigndlg.h"
0044 #include "ledgerpayeefilter.h"
0045 #include "ledgerviewsettings.h"
0046 #include "menuenums.h"
0047 #include "mymoneyaccount.h"
0048 #include "mymoneyaccountloan.h"
0049 #include "mymoneycontact.h"
0050 #include "mymoneyenums.h"
0051 #include "mymoneyexception.h"
0052 #include "mymoneyfile.h"
0053 #include "mymoneymoney.h"
0054 #include "mymoneypayee.h"
0055 #include "mymoneyprice.h"
0056 #include "mymoneyschedule.h"
0057 #include "mymoneysecurity.h"
0058 #include "mymoneysplit.h"
0059 #include "mymoneytransaction.h"
0060 #include "mymoneytransactionfilter.h"
0061 #include "payeesmodel.h"
0062 #include "specialdatesmodel.h"
0063 #include "specialledgeritemfilter.h"
0064 
0065 #include "ui_kpayeesview.h"
0066 
0067 using namespace Icons;
0068 
0069 enum filterTypeE { eAllPayees = 0, eReferencedPayees = 1, eUnusedPayees = 2 };
0070 
0071 class KPayeesViewPrivate : public KMyMoneyViewBasePrivate
0072 {
0073     Q_DECLARE_PUBLIC(KPayeesView)
0074 
0075 public:
0076     enum struct eTransactionDisplay {
0077         ShowTransactions,
0078         ClearTransactionDisplay,
0079     };
0080 
0081     explicit KPayeesViewPrivate(KPayeesView* qq)
0082         : KMyMoneyViewBasePrivate(qq)
0083         , ui(new Ui::KPayeesView)
0084         , m_transactionFilter(nullptr)
0085         , m_contact(nullptr)
0086         , m_syncedPayees(0)
0087         , m_filterProxyModel(nullptr)
0088         , m_renameProxyModel(nullptr)
0089     {
0090     }
0091 
0092     ~KPayeesViewPrivate()
0093     {
0094         // remember the splitter settings for startup
0095         auto grp = KSharedConfig::openConfig()->group("Last Use Settings");
0096         grp.writeEntry("KPayeesViewSplitterSize", ui->m_splitter->saveState());
0097         grp.sync();
0098         delete ui;
0099     }
0100 
0101     void init()
0102     {
0103         Q_Q(KPayeesView);
0104         ui->setupUi(q);
0105 
0106         ui->m_register->setSingleLineDetailRole(eMyMoney::Model::TransactionCounterAccountRole);
0107         ui->m_register->setColumnSelectorGroupName(QLatin1String("PayeeLedger"));
0108         ui->m_register->setShowPayeeInDetailColumn(false);
0109 
0110         // setup the model stack
0111         auto file = MyMoneyFile::instance();
0112         m_transactionFilter = new LedgerPayeeFilter(ui->m_register, QVector<QAbstractItemModel*> { file->specialDatesModel() });
0113         m_transactionFilter->setHideReconciledTransactions(LedgerViewSettings::instance()->hideReconciledTransactions());
0114         m_transactionFilter->setHideTransactionsBefore(LedgerViewSettings::instance()->hideTransactionsBefore());
0115 
0116         auto specialItemFilter = new SpecialLedgerItemFilter(q);
0117         specialItemFilter->setSourceModel(m_transactionFilter);
0118         ui->m_register->setModel(specialItemFilter);
0119 
0120         // keep track of changing balances
0121         q->connect(file->journalModel(), &JournalModel::balanceChanged, m_transactionFilter, &LedgerPayeeFilter::recalculateBalancesOnIdle);
0122 
0123         m_filterProxyModel = new AccountNamesFilterProxyModel(q);
0124         m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode());
0125         m_filterProxyModel->setHideZeroBalancedEquityAccounts(KMyMoneySettings::hideZeroBalanceEquities());
0126         m_filterProxyModel->setHideZeroBalancedAccounts(KMyMoneySettings::hideZeroBalanceAccounts());
0127         m_filterProxyModel->addAccountGroup(QVector<eMyMoney::Account::Type> {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity});
0128 
0129         m_filterProxyModel->setSourceModel(MyMoneyFile::instance()->accountsModel());
0130         m_filterProxyModel->sort(AccountsModel::Column::AccountName);
0131 
0132 
0133         ui->comboDefaultCategory->setModel(m_filterProxyModel);
0134 
0135         ui->matchTypeCombo->addItem(i18nc("@item No matching", "No matching"), static_cast<int>(eMyMoney::Payee::MatchType::Disabled));
0136         ui->matchTypeCombo->addItem(i18nc("@item Match Payees name partially", "Match Payees name (partial)"), static_cast<int>(eMyMoney::Payee::MatchType::Name));
0137         ui->matchTypeCombo->addItem(i18nc("@item Match Payees name exactly", "Match Payees name (exact)"), static_cast<int>(eMyMoney::Payee::MatchType::NameExact));
0138         ui->matchTypeCombo->addItem(i18nc("@item Search match in list", "Match on a name listed below"), static_cast<int>(eMyMoney::Payee::MatchType::Key));
0139 
0140         //load the filter type
0141         ui->m_filterBox->addItem(i18nc("@item Show all payees", "All"), ItemRenameProxyModel::eAllItem);
0142         ui->m_filterBox->addItem(i18nc("@item Show only used payees", "Used"), ItemRenameProxyModel::eReferencedItems);
0143         ui->m_filterBox->addItem(i18nc("@item Show only unused payees", "Unused"), ItemRenameProxyModel::eUnReferencedItems);
0144         ui->m_filterBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
0145 
0146         ui->m_updateButton->setIcon(Icons::get(Icon::DialogOK));
0147         ui->m_syncAddressbook->setIcon(Icons::get(Icon::Refresh));
0148         ui->m_sendMail->setIcon(Icons::get(Icon::MailMessage));
0149 
0150         ui->m_updateButton->setEnabled(false);
0151         ui->m_syncAddressbook->setEnabled(false);
0152 #ifndef ENABLE_ADDRESSBOOK
0153         ui->m_syncAddressbook->hide();
0154 #endif
0155         ui->matchTypeCombo->setCurrentIndex(0);
0156 
0157         ui->checkMatchIgnoreCase->setEnabled(false);
0158 
0159         ui->checkEnableDefaultCategory->setChecked(false);
0160         ui->labelDefaultCategory->setEnabled(false);
0161         ui->comboDefaultCategory->setEnabled(false);
0162 
0163         m_renameProxyModel = new ItemRenameProxyModel(q);
0164         ui->m_payees->setModel(m_renameProxyModel);
0165         ui->m_payees->setContextMenuPolicy(Qt::CustomContextMenu);
0166 
0167         m_renameProxyModel->setReferenceFilter(ItemRenameProxyModel::eAllItem);
0168         m_renameProxyModel->setFilterKeyColumn(PayeesModel::Column::Name);
0169         m_renameProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0170         m_renameProxyModel->setRenameColumn(PayeesModel::Column::Name);
0171         m_renameProxyModel->setSortRole(eMyMoney::Model::PayeeNameRole);
0172         m_renameProxyModel->setSortLocaleAware(true);
0173         m_renameProxyModel->sort(0);
0174 
0175         m_renameProxyModel->setSourceModel(MyMoneyFile::instance()->payeesModel());
0176 
0177         // setup the searchline widget
0178         q->connect(ui->m_searchWidget, &QLineEdit::textChanged, m_renameProxyModel, &QSortFilterProxyModel::setFilterFixedString);
0179         ui->m_searchWidget->setClearButtonEnabled(true);
0180         ui->m_searchWidget->setPlaceholderText(i18nc("Placeholder text", "Search"));
0181 
0182 #if 0
0183         ui->m_balanceLabel->hide();
0184 #endif
0185 
0186         m_contact = new MyMoneyContact(q);
0187         q->connect(m_contact, &MyMoneyContact::contactFetched, q, &KPayeesView::slotContactFetched);
0188 
0189         q->connect(ui->m_payees->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KPayeesView::slotPayeeSelectionChanged);
0190         ui->m_payees->setSelectionMode(QListView::ExtendedSelection);
0191         q->connect(m_renameProxyModel, &ItemRenameProxyModel::renameItem,  q, &KPayeesView::slotRenameSinglePayee);
0192         q->connect(m_renameProxyModel, &ItemRenameProxyModel::dataChanged, q, &KPayeesView::slotModelDataChanged);
0193 
0194         ui->m_newButton->setDefaultAction(pActions[eMenu::Action::NewPayee]);
0195         ui->m_renameButton->setDefaultAction(pActions[eMenu::Action::RenamePayee]);
0196         ui->m_deleteButton->setDefaultAction(pActions[eMenu::Action::DeletePayee]);
0197         ui->m_mergeButton->setDefaultAction(pActions[eMenu::Action::MergePayee]);
0198 
0199         q->connect(ui->addressEdit,   &QTextEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0200         q->connect(ui->payeecityEdit,  &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0201         q->connect(ui->payeestateEdit,  &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0202         q->connect(ui->postcodeEdit,  &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0203         q->connect(ui->telephoneEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0204         q->connect(ui->emailEdit,     &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0205         q->connect(ui->notesEdit,     &QTextEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged);
0206         q->connect(ui->payeeIdentifiers, &KPayeeIdentifierView::dataChanged, q, &KPayeesView::slotPayeeDataChanged);
0207         q->connect(ui->matchTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), q, &KPayeesView::slotPayeeDataChanged);
0208         q->connect(ui->checkMatchIgnoreCase, &QAbstractButton::toggled, q, &KPayeesView::slotPayeeDataChanged);
0209         q->connect(ui->checkEnableDefaultCategory,  &QAbstractButton::toggled, q,               &KPayeesView::slotPayeeDataChanged);
0210         q->connect(ui->comboDefaultCategory,        &KMyMoneyAccountCombo::accountSelected, q,  &KPayeesView::slotPayeeDataChanged);
0211 
0212         q->connect(ui->buttonSuggestACategory,      &QAbstractButton::clicked, q,               &KPayeesView::slotChooseDefaultAccount);
0213 
0214         q->connect(ui->matchKeyEditList, &KEditListWidget::changed, q, &KPayeesView::slotKeyListChanged);
0215 
0216         q->connect(ui->m_updateButton,    &QAbstractButton::clicked, q, &KPayeesView::slotUpdatePayee);
0217         q->connect(ui->m_syncAddressbook, &QAbstractButton::clicked, q, &KPayeesView::slotSyncAddressBook);
0218         q->connect(ui->m_helpButton,      &QAbstractButton::clicked, q, &KPayeesView::slotHelp);
0219         q->connect(ui->m_sendMail,        &QAbstractButton::clicked, q, &KPayeesView::slotSendMail);
0220 
0221 
0222         // use the size settings of the last run (if any)
0223         KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings");
0224         ui->m_splitter->restoreState(grp.readEntry("KPayeesViewSplitterSize", QByteArray()));
0225         ui->m_splitter->setChildrenCollapsible(false);
0226 
0227         QVector<int> columns;
0228         columns = {
0229             JournalModel::Column::Number,
0230             JournalModel::Column::Payee,
0231             JournalModel::Column::Security,
0232             JournalModel::Column::CostCenter,
0233             JournalModel::Column::Quantity,
0234             JournalModel::Column::Price,
0235             JournalModel::Column::Amount,
0236             JournalModel::Column::Value,
0237             JournalModel::Column::Balance,
0238         };
0239         ui->m_register->setColumnsHidden(columns);
0240         columns = {
0241             JournalModel::Column::Date,
0242             JournalModel::Column::Account,
0243             JournalModel::Column::Detail,
0244             JournalModel::Column::Reconciliation,
0245             JournalModel::Column::Payment,
0246             JournalModel::Column::Deposit,
0247         };
0248         ui->m_register->setColumnsShown(columns);
0249 
0250         // At start we haven't any payee selected
0251         ui->m_tabWidget->setEnabled(false); // disable tab widget
0252 
0253         m_payee = MyMoneyPayee(); // make sure we don't access an undefined payee
0254         clearItemData();
0255 
0256         m_focusWidget = ui->m_searchWidget;
0257 
0258         specialItemFilter->setSortingEnabled(true);
0259     }
0260 
0261     void ensurePayeeVisible(const QString& id)
0262     {
0263         const auto baseIdx = MyMoneyFile::instance()->payeesModel()->indexById(id);
0264         if (baseIdx.isValid()) {
0265             const auto idx = MyMoneyFile::baseModel()->mapFromBaseSource(m_renameProxyModel, baseIdx);
0266             ui->m_payees->setCurrentIndex(idx);
0267             ui->m_payees->scrollTo(idx);
0268         }
0269     }
0270 
0271     void loadDetails()
0272     {
0273         ui->addressEdit->setText(m_payee.address());
0274         ui->postcodeEdit->setText(m_payee.postcode());
0275         ui->telephoneEdit->setText(m_payee.telephone());
0276         ui->emailEdit->setText(m_payee.email());
0277         ui->notesEdit->setText(m_payee.notes());
0278         ui->payeestateEdit->setText(m_payee.state());
0279         ui->payeecityEdit->setText(m_payee.city());
0280 
0281         QStringList keys;
0282         bool ignorecase = false;
0283         auto type = m_payee.matchData(ignorecase, keys);
0284 
0285         ui->matchTypeCombo->setCurrentIndex(ui->matchTypeCombo->findData(static_cast<int>(type)));
0286         ui->matchKeyEditList->clear();
0287         ui->matchKeyEditList->insertStringList(keys);
0288         ui->checkMatchIgnoreCase->setChecked(ignorecase);
0289 
0290         ui->comboDefaultCategory->clearSelection();
0291         ui->checkEnableDefaultCategory->setChecked(!m_payee.defaultAccountId().isEmpty());
0292         ui->comboDefaultCategory->setSelected(m_payee.defaultAccountId());
0293 
0294         ui->payeeIdentifiers->setSource(m_payee);
0295 
0296         ui->m_tabWidget->setEnabled(!m_payee.id().isEmpty());
0297     }
0298 
0299     void clearItemData()
0300     {
0301         ui->addressEdit->setText(QString());
0302         ui->payeecityEdit->setText(QString());
0303         ui->payeestateEdit->setText(QString());
0304         ui->postcodeEdit->setText(QString());
0305         ui->telephoneEdit->setText(QString());
0306         ui->emailEdit->setText(QString());
0307         ui->notesEdit->setText(QString());
0308         showTransactions(eTransactionDisplay::ClearTransactionDisplay);
0309     }
0310 
0311     QStringList selectedPayeeIds() const
0312     {
0313         QStringList payees;
0314         auto baseModel = MyMoneyFile::instance()->payeesModel();
0315         const QModelIndexList selection = ui->m_payees->selectionModel()->selectedIndexes();
0316         for (const auto idx : selection) {
0317             auto baseIdx = baseModel->mapToBaseSource(idx);
0318             const auto id = baseIdx.data(eMyMoney::Model::IdRole).toString();
0319             if (!id.isEmpty()) {
0320                 payees.append(id);
0321             }
0322         }
0323         return payees;
0324     }
0325 
0326     QList<MyMoneyPayee> selectedPayees() const
0327     {
0328         QList<MyMoneyPayee> payees;
0329         auto baseModel = MyMoneyFile::instance()->payeesModel();
0330         const QModelIndexList selection = ui->m_payees->selectionModel()->selectedIndexes();
0331         for (const auto idx : selection) {
0332             auto baseIdx = baseModel->mapToBaseSource(idx);
0333             auto payee = baseModel->itemByIndex(baseIdx);
0334             if (!payee.id().isEmpty()) {
0335                 payees.append(payee);
0336             }
0337         }
0338         return payees;
0339     }
0340 
0341     void showTransactions(eTransactionDisplay clearDisplay = eTransactionDisplay::ShowTransactions)
0342     {
0343         MyMoneyMoney balance;
0344         const auto file = MyMoneyFile::instance();
0345         MyMoneySecurity base = file->baseCurrency();
0346 
0347         const auto selection = selectedPayees();
0348 
0349         QStringList payeeIds;
0350         if (selection.isEmpty() || !ui->m_tabWidget->isEnabled()) {
0351             m_transactionFilter->setPayeeIdList(payeeIds);
0352             ui->m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction())));
0353             return;
0354         }
0355 
0356         if (clearDisplay == eTransactionDisplay::ShowTransactions) {
0357             for (const auto& payee : selection) {
0358                 payeeIds.append(payee.id());
0359             }
0360         }
0361         m_transactionFilter->setPayeeIdList(payeeIds);
0362 
0363         bool balanceAccurate = true;
0364         QModelIndex idx;
0365         MyMoneyMoney deposit, payment;
0366         const auto rows = m_transactionFilter->rowCount();
0367         for(int row = 0; row < rows; ++row) {
0368             idx = m_transactionFilter->index(row, 0);
0369             const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0370             // in case of an entry of the specialDatesModel the accountId is empty
0371             if (!accountId.isEmpty()) {
0372                 const auto acc = file->account(accountId);
0373 
0374                 const auto shares = idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>();
0375                 auto val = shares.abs();
0376                 if (acc.currencyId() != base.id()) {
0377                     const MyMoneyPrice &price = file->price(acc.currencyId(), base.id());
0378                     // in case the price is valid, we use it. Otherwise, we keep
0379                     // a flag that tells us that the balance is somewhat inaccurate
0380                     if (price.isValid()) {
0381                         val *= price.rate(base.id());
0382                     } else {
0383                         balanceAccurate = false;
0384                     }
0385                 }
0386 
0387                 if (shares.isNegative()) {
0388                     payment += val;
0389                 } else {
0390                     deposit += val;
0391                 }
0392             }
0393         }
0394         balance = deposit - payment;
0395 
0396         ui->m_balanceLabel->setText(i18n("Balance: %1%2",
0397                                          balanceAccurate ? QString() : QStringLiteral("~"),
0398                                          balance.formatMoney(file->baseCurrency().smallestAccountFraction())));
0399     }
0400 
0401     /**
0402       * Implement common task when deleting or merging payees
0403       */
0404     bool payeeReassign(int type)
0405     {
0406         Q_Q(KPayeesView);
0407         if (!(type >= 0 && type < KPayeeReassignDlg::TypeCount))
0408             return false;
0409 
0410         const auto file = MyMoneyFile::instance();
0411 
0412         MyMoneyFileTransaction ft;
0413         try {
0414             // create a transaction filter that contains all payees selected for removal
0415             MyMoneyTransactionFilter f = MyMoneyTransactionFilter();
0416             const auto list = selectedPayees();
0417             for (const auto& payee : list) {
0418                 f.addPayee(payee.id());
0419             }
0420             f.setConsiderCategorySplits(true);
0421 
0422             // request a list of all transactions that still use the payees in question
0423             QList<MyMoneyTransaction> translist;
0424             file->transactionList(translist, f);
0425             //     qDebug() << "[KPayeesView::slotDeletePayee]  " << translist.count() << " transaction still assigned to payees";
0426 
0427             // now get a list of all schedules that make use of one of the payees
0428             QList<MyMoneySchedule> used_schedules;
0429             for (const auto& schedule : file->scheduleList()) {
0430                 // loop over all splits in the transaction of the schedule
0431                 const auto splits = schedule.transaction().splits();
0432                 for (const auto& split : splits) {
0433                     // is the payee in the split to be deleted?
0434                     if (payeeInList(list, split.payeeId())) {
0435                         used_schedules.push_back(schedule); // remember this schedule
0436                         break;
0437                     }
0438                 }
0439             }
0440             //     qDebug() << "[KPayeesView::slotDeletePayee]  " << used_schedules.count() << " schedules use one of the selected payees";
0441 
0442             // and a list of all loan accounts that references one of the payees
0443             QList<MyMoneyAccount> allAccounts;
0444             QList<MyMoneyAccount> usedAccounts;
0445             file->accountList(allAccounts);
0446             for (const MyMoneyAccount &account : allAccounts) {
0447                 if (account.isLoan()) {
0448                     MyMoneyAccountLoan loanAccount(account);
0449                     for (const MyMoneyPayee &payee : list) {
0450                         if (loanAccount.hasReferenceTo(payee.id())) {
0451                             usedAccounts.append(account);
0452                         }
0453                     }
0454                 }
0455             }
0456 
0457 
0458             MyMoneyPayee newPayee;
0459             bool addToMatchList = false;
0460             // if at least one payee is still referenced, we need to reassign its transactions first
0461             if (!translist.isEmpty() || !used_schedules.isEmpty() || !usedAccounts.isEmpty()) {
0462 
0463                 // first create list with all non-selected payees
0464                 QList<MyMoneyPayee> remainingPayees;
0465                 if (type == KPayeeReassignDlg::TypeMerge) {
0466                     remainingPayees = list;
0467                 } else {
0468                     remainingPayees = file->payeeList();
0469                     QList<MyMoneyPayee>::iterator it_p;
0470                     for (it_p = remainingPayees.begin(); it_p != remainingPayees.end();) {
0471                         if (list.contains(*it_p)) {
0472                             it_p = remainingPayees.erase(it_p);
0473                         } else {
0474                             ++it_p;
0475                         }
0476                     }
0477                 }
0478 
0479                 // show error message if no payees remain
0480                 if (remainingPayees.isEmpty()) {
0481                     KMessageBox::error(q, i18n("At least one transaction/scheduled transaction or loan account is still referenced by a payee. "
0482                                                "Currently you have all payees selected. However, at least one payee must remain so "
0483                                                "that the transaction/scheduled transaction or loan account can be reassigned."));
0484                     return false;
0485                 }
0486 
0487                 // show transaction reassignment dialog
0488                 KPayeeReassignDlg * dlg = new KPayeeReassignDlg(static_cast<KPayeeReassignDlg::OperationType>(type), q);
0489                 KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart());
0490                 QString payee_id = dlg->show(remainingPayees);
0491                 addToMatchList = dlg->addToMatchList();
0492                 delete dlg; // and kill the dialog
0493                 if (payee_id.isEmpty())
0494                     return false; // the user aborted the dialog, so let's abort as well
0495 
0496                 // try to get selected payee. If not possible and we are merging payees,
0497                 // then we create a new one
0498                 try {
0499                     newPayee = file->payee(payee_id);
0500                 } catch (const MyMoneyException &) {
0501                     if (type == KPayeeReassignDlg::TypeMerge) {
0502                         // it's ok to use payee_id for both arguments since the first is const,
0503                         // so it's guaranteed not to change its content
0504                         bool ok;
0505                         std::tie(ok, payee_id) = KMyMoneyUtils::newPayee(payee_id);
0506                         if (!ok)
0507                             return false; // the user aborted the dialog, so let's abort as well
0508                         newPayee = file->payee(payee_id);
0509                     } else {
0510                         return false;
0511                     }
0512                 }
0513 
0514                 // TODO : check if we have a report that explicitly uses one of our payees
0515                 //        and issue an appropriate warning
0516                 try {
0517                     // now loop over all transactions and reassign payee
0518                     for (auto& transaction : translist) {
0519                         // create a copy of the splits list in the transaction
0520                         // loop over all splits
0521                         for (auto& split : transaction.splits()) {
0522                             // if the split is assigned to one of the selected payees, we need to modify it
0523                             if (split.isMatched()) {
0524                                 auto tm = split.matchedTransaction();
0525                                 for (auto& sm : tm.splits()) {
0526                                     if (payeeInList(list, sm.payeeId())) {
0527                                         sm.setPayeeId(payee_id); // first modify payee in current split
0528                                         // then modify the split in our local copy of the transaction list
0529                                         tm.modifySplit(sm); // this does not modify the list object 'splits'!
0530                                     }
0531                                 }
0532                                 split.addMatch(tm);
0533                                 transaction.modifySplit(split); // this does not modify the list object 'splits'!
0534                             }
0535                             if (payeeInList(list, split.payeeId())) {
0536                                 split.setPayeeId(payee_id); // first modify payee in current split
0537                                 // then modify the split in our local copy of the transaction list
0538                                 transaction.modifySplit(split); // this does not modify the list object 'splits'!
0539                             }
0540                         } // for - Splits
0541                         file->modifyTransaction(transaction);  // modify the transaction in the MyMoney object
0542                     } // for - Transactions
0543 
0544                     // now loop over all schedules and reassign payees
0545                     for (auto& schedule : used_schedules) {
0546                         // create copy of transaction in current schedule
0547                         auto trans = schedule.transaction();
0548                         // create copy of lists of splits
0549                         for (auto& split : trans.splits()) {
0550                             if (payeeInList(list, split.payeeId())) {
0551                                 split.setPayeeId(payee_id);
0552                                 trans.modifySplit(split); // does not modify the list object 'splits'!
0553                             }
0554                         } // for - Splits
0555                         // store transaction in current schedule
0556                         schedule.setTransaction(trans);
0557                         file->modifySchedule(schedule);  // modify the schedule in the MyMoney engine
0558                     } // for - Schedules
0559 
0560                     // reassign the payees in the loans that reference the deleted payees
0561                     for (const MyMoneyAccount& account : qAsConst(usedAccounts)) {
0562                         MyMoneyAccountLoan loanAccount(account);
0563                         loanAccount.setPayee(payee_id);
0564                         file->modifyAccount(loanAccount);
0565                     }
0566 
0567                 } catch (const MyMoneyException &e) {
0568                     KMessageBox::detailedError(q, i18n("Unable to reassign payee of transaction/split"), e.what());
0569                 }
0570             } else { // if !translist.isEmpty()
0571                 if (type == KPayeeReassignDlg::TypeMerge) {
0572                     KMessageBox::error(q, i18n("Nothing to merge."), i18n("Merge Payees"));
0573                     return false;
0574                 }
0575             }
0576 
0577             bool ignorecase;
0578             QStringList payeeNames;
0579             auto matchType = newPayee.matchData(ignorecase, payeeNames);
0580             QStringList deletedMatchPattern;
0581 
0582             // now loop over all selected payees and remove them
0583             for (QList<MyMoneyPayee>::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) {
0584                 if (newPayee.id() != (*it).id()) {
0585                     if (addToMatchList) {
0586                         QStringList matchPattern;
0587                         auto mType = (*it).matchData(ignorecase, matchPattern);
0588                         switch (mType) {
0589                         case eMyMoney::Payee::MatchType::NameExact:
0590                             matchPattern << QStringLiteral("^%1$").arg((*it).name());
0591                             break;
0592                         case eMyMoney::Payee::MatchType::Name:
0593                             matchPattern << (*it).name();
0594                             break;
0595                         default:
0596                             break;
0597                         }
0598                         if (!matchPattern.isEmpty()) {
0599                             deletedMatchPattern << matchPattern;
0600                         }
0601                     }
0602                     file->removePayee(*it);
0603                 }
0604             }
0605 
0606             // if we initially have no matching turned on, we just ignore the case (default)
0607             if (matchType == eMyMoney::Payee::MatchType::Disabled)
0608                 ignorecase = true;
0609 
0610             // update the destination payee if this was requested by the user
0611             if (addToMatchList && deletedMatchPattern.count() > 0) {
0612                 // add new names to the list
0613                 // TODO: it would be cool to somehow shrink the list to make better use
0614                 //       of regular expressions at this point. For now, we leave this task
0615                 //       to the user himeself.
0616 
0617                 // the deletedMatchPattern contains entries of MatchType::Key. Let's
0618                 // check if the destination entry needs to be converted to this formatMoney
0619                 switch (matchType) {
0620                 case eMyMoney::Payee::MatchType::Disabled:
0621                 case eMyMoney::Payee::MatchType::Key:
0622                     break;
0623                 case eMyMoney::Payee::MatchType::Name:
0624                     payeeNames.clear();
0625                     payeeNames << newPayee.name();
0626                     break;
0627                 case eMyMoney::Payee::MatchType::NameExact:
0628                     payeeNames.clear();
0629                     payeeNames << QStringLiteral("^%1$").arg(newPayee.name());
0630                     break;
0631                 }
0632 
0633                 QStringList::const_iterator it_n;
0634                 for (it_n = deletedMatchPattern.constBegin(); it_n != deletedMatchPattern.constEnd(); ++it_n) {
0635                     // make sure we really need it and it is not caught by an existing regexp
0636                     QStringList::const_iterator it_k;
0637                     for (it_k = payeeNames.constBegin(); it_k != payeeNames.constEnd(); ++it_k) {
0638                         const QRegularExpression exp(*it_k, ignorecase ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption);
0639                         const auto payee(exp.match(*it_n));
0640                         if (payee.hasMatch())
0641                             break;
0642                     }
0643                     if (it_k == payeeNames.constEnd())
0644                         payeeNames << *it_n;
0645                 }
0646 
0647                 // and update the payee in the engine context
0648                 // make sure to turn on matching for this payee in the right mode
0649                 newPayee.setMatchData(eMyMoney::Payee::MatchType::Key, ignorecase, payeeNames);
0650                 file->modifyPayee(newPayee);
0651             }
0652             ft.commit();
0653 
0654         } catch (const MyMoneyException &e) {
0655             KMessageBox::detailedError(q, i18n("Unable to remove payee(s)"), e.what());
0656         }
0657 
0658         return true;
0659     }
0660 
0661     /**
0662       * Check if a list contains a payee with a given id
0663       *
0664       * @param list const reference to value list
0665       * @param id const reference to id
0666       *
0667       * @retval true object has been found
0668       * @retval false object is not in list
0669       */
0670     bool payeeInList(const QList<MyMoneyPayee>& list, const QString& id) const
0671     {
0672         for (const auto& payee : list) {
0673             if (payee.id() == id) {
0674                 return true;
0675             }
0676         }
0677         return false;
0678     }
0679 
0680     /** Checks whether the currently selected payee is "dirty"
0681      * @return true, if payee is modified (is "dirty"); false otherwise
0682      */
0683     bool isDirty() const
0684     {
0685         return ui->m_updateButton->isEnabled();
0686     }
0687 
0688     /** Sets the payee's "dirty" (modified) status
0689      * @param dirty if true (default), payee will be set to dirty
0690      */
0691     void setDirty(bool dirty)
0692     {
0693         ui->m_updateButton->setEnabled(dirty);
0694     }
0695 
0696     void selectPayeeAndTransaction(const QString& payeeId, const QString& accountId = QString(), const QString& journalEntryId = QString())
0697     {
0698         Q_Q(KPayeesView);
0699         if (!q->isVisible())
0700             return;
0701 
0702         const auto file = MyMoneyFile::instance();
0703         QModelIndex idx;
0704         QModelIndexList list;
0705         do {
0706             list = ui->m_payees->model()->match(ui->m_payees->model()->index(0, 0),
0707                                                 eMyMoney::Model::IdRole,
0708                                                 payeeId,
0709                                                 -1, // all splits
0710                                                 Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
0711 
0712             if (list.isEmpty()) {
0713                 if (!ui->m_searchWidget->text().isEmpty()) {
0714                     ui->m_searchWidget->clear();
0715                     continue;
0716                 }
0717                 if (m_renameProxyModel->referenceFilter() != ItemRenameProxyModel::eAllItem) {
0718                     m_renameProxyModel->setReferenceFilter(ItemRenameProxyModel::eAllItem);
0719                     continue;
0720                 }
0721             }
0722             break;
0723         } while (true);
0724 
0725         if (list.isEmpty()) {
0726             qDebug() << "selectPayeeAndTransaction: Payee not found" << payeeId;
0727             return;
0728         }
0729 
0730         m_selections.clearSelections();
0731         m_selections.setSelection(SelectedObjects::Payee, payeeId);
0732 
0733         // select the payee
0734         ui->m_payees->setCurrentIndex(list.at(0));
0735 
0736         const auto model = ui->m_register->model();
0737         Q_ASSERT(model != nullptr);
0738 
0739         const auto rows = model->rowCount();
0740         int transactionRow = -1;
0741 
0742         // it could be, that the selected JournalEntryId is not available here
0743         // so we also scan for the transactionId
0744 
0745         const auto journalIdx = file->journalModel()->indexById(journalEntryId);
0746         const auto transactionId = journalIdx.data(eMyMoney::Model::JournalTransactionIdRole).toString();
0747 
0748         // scan all entries shown for this payee
0749         for (int row = 0; row < rows; ++row) {
0750             idx = model->index(row, 0);
0751             if (idx.data(eMyMoney::Model::IdRole).toString() == journalEntryId) {
0752                 transactionRow = row;
0753                 break;
0754 
0755             } else if (idx.data(eMyMoney::Model::JournalTransactionIdRole).toString() == transactionId) {
0756                 // if the transaction id matches and we don't have a match already we keep it
0757                 if (transactionRow == -1)
0758                     transactionRow = row;
0759                 // in case the account is available and it matches, we use it right away
0760                 if (!accountId.isEmpty() && (idx.data(eMyMoney::Model::SplitAccountIdRole).toString() == accountId)) {
0761                     transactionRow = row;
0762                     break;
0763                 }
0764             } else if (transactionRow != -1) {
0765                 // if we have a match for a transaction, we use it
0766                 break;
0767             }
0768         }
0769 
0770         ensurePayeeVisible(payeeId);
0771 
0772         if (transactionRow != -1) {
0773             // use the date column here for the index so that scrollTo has an effect
0774             // because column 0 (Number) is hidden in the payees view.
0775             idx = model->index(transactionRow, JournalModel::Column::Date);
0776             ui->m_register->setCurrentIndex(idx);
0777 
0778             m_selections.setSelection(SelectedObjects::Account, idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0779             m_selections.setSelection(SelectedObjects::JournalEntry, idx.data(eMyMoney::Model::IdRole).toString());
0780 
0781             // if it's not the last transaction, we scroll a bit further
0782             if (transactionRow + 1 < rows) {
0783                 idx = model->index(transactionRow + 1, JournalModel::Column::Date);
0784             }
0785             ui->m_register->scrollTo(idx);
0786         }
0787 
0788         q->Q_EMIT requestSelectionChange(m_selections);
0789     }
0790 
0791     void finalizePendingChanges()
0792     {
0793         Q_Q(KPayeesView);
0794         if (isDirty()) {
0795             if (KMessageBox::questionTwoActions(q,
0796                                                 i18n("<qt>Do you want to save the changes for <b>%1</b>?</qt>", m_newName),
0797                                                 i18n("Save changes"),
0798                                                 KMMYesNo::yes(),
0799                                                 KMMYesNo::no())
0800                 == KMessageBox::PrimaryAction) {
0801                 q->slotUpdatePayee();
0802             }
0803         }
0804     }
0805 
0806     void selectPayee()
0807     {
0808         Q_Q(KPayeesView);
0809         const auto selectedPayeesList = selectedPayees();
0810         m_payee = MyMoneyPayee();
0811         if (!selectedPayeesList.isEmpty()) {
0812             m_payee = selectedPayeesList.at(0);
0813         }
0814 
0815         // as of now we are updating only the last selected payee, and until
0816         // selection mode of the QListView has been changed to Extended, this
0817         // will also be the only selection and behave exactly as before - Andreas
0818         try {
0819             m_newName = m_payee.name();
0820             loadDetails();
0821 
0822             q->slotPayeeDataChanged();
0823             showTransactions();
0824 
0825         } catch (const MyMoneyException& e) {
0826             qDebug() << "exception during display of payee: " << e.what();
0827             // clear display data
0828             m_transactionFilter->setPayeeIdList(QStringList());
0829             m_payee = MyMoneyPayee();
0830         }
0831 
0832         m_selections.setSelection(SelectedObjects::Payee, selectedPayeeIds());
0833         q->Q_EMIT requestSelectionChange(m_selections);
0834     }
0835 
0836     Ui::KPayeesView*    ui;
0837     LedgerPayeeFilter*  m_transactionFilter;
0838 
0839     MyMoneyPayee        m_payee;
0840     QString             m_newName;
0841     MyMoneyContact*     m_contact;
0842     int                 m_syncedPayees;
0843     QList<MyMoneyPayee> m_payeesToSync;
0844 
0845     AccountNamesFilterProxyModel *m_filterProxyModel;
0846     ItemRenameProxyModel*         m_renameProxyModel;
0847 };
0848 
0849 #endif