File indexing completed on 2024-05-12 05:07:49

0001 /*
0002     SPDX-FileCopyrightText: 2015-2020 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 
0007 #include "newtransactioneditor.h"
0008 
0009 // ----------------------------------------------------------------------------
0010 // QT Includes
0011 
0012 #include <QAbstractItemView>
0013 #include <QCompleter>
0014 #include <QDebug>
0015 #include <QGlobalStatic>
0016 #include <QHeaderView>
0017 #include <QSortFilterProxyModel>
0018 #include <QStandardItemModel>
0019 #include <QStringList>
0020 #include <QTableView>
0021 #include <QTimer>
0022 
0023 // ----------------------------------------------------------------------------
0024 // KDE Includes
0025 
0026 #include <KLocalizedString>
0027 
0028 // ----------------------------------------------------------------------------
0029 // Project Includes
0030 
0031 #include "costcentermodel.h"
0032 #include "icons.h"
0033 #include "idfilter.h"
0034 #include "journalmodel.h"
0035 #include "kcurrencycalculator.h"
0036 #include "kmymoneysettings.h"
0037 #include "kmymoneyutils.h"
0038 #include "knewaccountdlg.h"
0039 #include "ktransactionselectdlg.h"
0040 #include "mymoneyaccount.h"
0041 #include "mymoneyexception.h"
0042 #include "mymoneyfile.h"
0043 #include "mymoneypayee.h"
0044 #include "mymoneyschedule.h"
0045 #include "mymoneysecurity.h"
0046 #include "mymoneysplit.h"
0047 #include "mymoneytransaction.h"
0048 #include "payeesmodel.h"
0049 #include "securitiesmodel.h"
0050 #include "splitdialog.h"
0051 #include "splitmodel.h"
0052 #include "statusmodel.h"
0053 #include "tagsmodel.h"
0054 #include "widgethintframe.h"
0055 
0056 #include "ui_newtransactioneditor.h"
0057 
0058 using namespace Icons;
0059 
0060 class NewTransactionEditor::Private
0061 {
0062     Q_DISABLE_COPY_MOVE(Private)
0063 
0064 public:
0065     enum TaxValueChange {
0066         ValueUnchanged,
0067         ValueChanged,
0068     };
0069     Private(NewTransactionEditor* parent)
0070         : q(parent)
0071         , ui(new Ui_NewTransactionEditor)
0072         , tabOrderUi(nullptr)
0073         , accountsModel(new AccountNamesFilterProxyModel(parent))
0074         , categoriesModel(new AccountNamesFilterProxyModel(parent))
0075         , costCenterModel(new QSortFilterProxyModel(parent))
0076         , payeesModel(new QSortFilterProxyModel(parent))
0077         , costCenterRequired(false)
0078         , inUpdateVat(false)
0079         , keepCategoryAmount(false)
0080         , loadedFromModel(false)
0081         , splitModel(parent, &undoStack)
0082         , frameCollection(nullptr)
0083         , m_splitHelper(nullptr)
0084     {
0085         accountsModel->setObjectName(QLatin1String("NewTransactionEditor::accountsModel"));
0086         categoriesModel->setObjectName(QLatin1String("NewTransactionEditor::categoriesModel"));
0087         costCenterModel->setObjectName(QLatin1String("SortedCostCenterModel"));
0088         payeesModel->setObjectName(QLatin1String("SortedPayeesModel"));
0089         splitModel.setObjectName(QLatin1String("SplitModel"));
0090 
0091         costCenterModel->setSortLocaleAware(true);
0092         costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0093 
0094         payeesModel->setSortLocaleAware(true);
0095         payeesModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0096     }
0097 
0098     ~Private()
0099     {
0100         delete ui;
0101     }
0102 
0103     void updateWidgetState();
0104     void setupTabOrder();
0105     bool checkForValidTransaction(bool doUserInteraction = true);
0106     bool isDatePostOpeningDate(const QDate& date, const QString& accountId);
0107     bool postdateChanged(const QDate& date);
0108     bool costCenterChanged(int costCenterIndex);
0109     void payeeChanged(int payeeIndex);
0110     void autoFillTransaction(const QString& payeeId);
0111     void accountChanged(const QString& id);
0112     bool categoryChanged(const QString& id);
0113     bool numberChanged(const QString& newNumber);
0114     bool amountChanged();
0115     bool isIncomeExpense(const QModelIndex& idx) const;
0116     bool isIncomeExpense(const QString& categoryId) const;
0117     bool tagsChanged(const QStringList& ids);
0118     int editSplits();
0119     void updateWidgetAccess();
0120     void updateVAT(TaxValueChange amountChanged);
0121     MyMoneyMoney removeVatSplit();
0122     MyMoneyMoney splitsSum() const;
0123     void defaultCategoryAssignment();
0124     void loadTransaction(QModelIndex idx);
0125     MyMoneySplit prepareSplit(const MyMoneySplit& sp);
0126     bool needClearSplitAction(const QString& action) const;
0127     void adjustTagIdList();
0128 
0129     NewTransactionEditor* q;
0130     Ui_NewTransactionEditor* ui;
0131     Ui_NewTransactionEditor* tabOrderUi;
0132     AccountNamesFilterProxyModel* accountsModel;
0133     AccountNamesFilterProxyModel* categoriesModel;
0134     QSortFilterProxyModel* costCenterModel;
0135     QSortFilterProxyModel* payeesModel;
0136     bool costCenterRequired;
0137     bool inUpdateVat;
0138     bool keepCategoryAmount;
0139     bool loadedFromModel;
0140     QUndoStack undoStack;
0141     SplitModel splitModel;
0142     MyMoneyAccount m_account;
0143     MyMoneyTransaction m_transaction;
0144     MyMoneySplit m_split;
0145     WidgetHintFrameCollection* frameCollection;
0146     KMyMoneyAccountComboSplitHelper* m_splitHelper;
0147 };
0148 
0149 void NewTransactionEditor::Private::adjustTagIdList()
0150 {
0151     // if we open a transaction in an asset or liability account and if the transaction
0152     // has more than 2 splits and the current split has a taglist,
0153     // a) if the other splits don't have a taglist assigned, copy it over to them
0154     // b) clear the taglist in the current split
0155 
0156     if (m_account.isAssetLiability() && !m_split.tagIdList().isEmpty()) {
0157         const auto rows = splitModel.rowCount();
0158         for (int row = 0; row < rows; ++row) {
0159             const auto idx = splitModel.index(row, 0);
0160             if (idx.data(eMyMoney::Model::SplitTagIdRole).toStringList().isEmpty()) {
0161                 splitModel.setData(idx, QVariant::fromValue<QStringList>(m_split.tagIdList()), eMyMoney::Model::SplitTagIdRole);
0162             }
0163         }
0164         m_split.setTagIdList({});
0165     }
0166 }
0167 
0168 void NewTransactionEditor::Private::updateWidgetAccess()
0169 {
0170     const auto enable = !m_account.id().isEmpty();
0171     ui->dateEdit->setEnabled(enable);
0172     ui->creditDebitEdit->setEnabled(enable);
0173     ui->payeeEdit->setEnabled(enable);
0174     ui->numberEdit->setEnabled(enable);
0175     ui->categoryCombo->setEnabled(enable);
0176     ui->costCenterCombo->setEnabled(enable);
0177     ui->tagContainer->setEnabled(enable);
0178     ui->statusCombo->setEnabled(enable);
0179     ui->memoEdit->setEnabled(enable);
0180     ui->enterButton->setEnabled(!q->isReadOnly());
0181 }
0182 
0183 void NewTransactionEditor::Private::updateWidgetState()
0184 {
0185     auto index = splitModel.index(0, 0);
0186 
0187     // update the tag combo box
0188     if (splitModel.rowCount() == 1) {
0189         ui->tagContainer->setEnabled(true);
0190         ui->tagContainer->loadTags(index.data(eMyMoney::Model::SplitTagIdRole).toStringList());
0191     } else {
0192         ui->tagContainer->setEnabled(false);
0193         ui->tagContainer->loadTags({});
0194     }
0195 
0196     // update the costcenter combo box
0197     if (ui->costCenterCombo->isEnabled()) {
0198         // extract the cost center
0199         index = MyMoneyFile::instance()->costCenterModel()->indexById(index.data(eMyMoney::Model::SplitCostCenterIdRole).toString());
0200         if (index.isValid())
0201             ui->costCenterCombo->setCurrentIndex(costCenterModel->mapFromSource(index).row());
0202     }
0203 }
0204 
0205 bool NewTransactionEditor::Private::checkForValidTransaction(bool doUserInteraction)
0206 {
0207     QStringList infos;
0208     bool rc = true;
0209     if (!postdateChanged(ui->dateEdit->date())) {
0210         infos << ui->dateEdit->toolTip();
0211         rc = false;
0212     }
0213 
0214     if (!costCenterChanged(ui->costCenterCombo->currentIndex())) {
0215         infos << ui->costCenterCombo->toolTip();
0216         rc = false;
0217     }
0218 
0219     if (q->needCreateCategory(ui->categoryCombo) || q->needCreatePayee(ui->payeeEdit)) {
0220         rc = false;
0221     }
0222 
0223     if (doUserInteraction) {
0224         /// @todo add dialog here that shows the @a infos about the problem
0225     }
0226     return rc;
0227 }
0228 
0229 bool NewTransactionEditor::Private::isDatePostOpeningDate(const QDate& date, const QString& accountId)
0230 {
0231     bool rc = true;
0232 
0233     try {
0234         MyMoneyAccount account = MyMoneyFile::instance()->account(accountId);
0235         const bool isIncomeExpense = account.isIncomeExpense();
0236 
0237         // we don't check for categories
0238         if (!isIncomeExpense) {
0239             if (date < account.openingDate())
0240                 rc = false;
0241         }
0242     } catch (MyMoneyException&) {
0243         qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO;
0244     }
0245     return rc;
0246 }
0247 
0248 /**
0249  * Check that the postdate is valid and that all referenced
0250  * account's opening date is prior to the postdate. Return
0251  * @a true if all conditions are met.
0252  */
0253 bool NewTransactionEditor::Private::postdateChanged(const QDate& date)
0254 {
0255     WidgetHintFrame::hide(ui->dateEdit, i18n("The posting date of the transaction."));
0256 
0257     if (!date.isValid()) {
0258         WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is invalid."));
0259         return false;
0260     }
0261 
0262     // collect all account ids
0263     QStringList accountIds;
0264     accountIds << m_account.id();
0265     const auto rows = splitModel.rowCount();
0266     for (int row = 0; row < rows; ++row) {
0267         const auto index = splitModel.index(row, 0);
0268         accountIds << index.data(eMyMoney::Model::SplitAccountIdRole).toString();
0269     }
0270 
0271     bool rc = true;
0272     for (const auto& accountId : accountIds) {
0273         if (!isDatePostOpeningDate(date, accountId)) {
0274             MyMoneyAccount account = MyMoneyFile::instance()->account(accountId);
0275             WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is prior to the opening date of account <b>%1</b>.", account.name()));
0276             rc = false;
0277             break;
0278         }
0279     }
0280     return rc;
0281 }
0282 
0283 /**
0284  * Check that the cost center information is filled when
0285  * required for the category and update the first split
0286  * of a normal transaction with the id of the selected
0287  * cost center. Returns @a true if cost center assignment
0288  * is correct.
0289  */
0290 bool NewTransactionEditor::Private::costCenterChanged(int costCenterIndex)
0291 {
0292     bool rc = true;
0293     WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to."));
0294     if (costCenterIndex != -1) {
0295         if (costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) {
0296             WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category."));
0297             rc = false;
0298         }
0299         if (rc == true && splitModel.rowCount() == 1) {
0300             auto index = costCenterModel->index(costCenterIndex, 0);
0301             const auto costCenterId = index.data(eMyMoney::Model::IdRole).toString();
0302             index = splitModel.index(0, 0);
0303 
0304             splitModel.setData(index, costCenterId, eMyMoney::Model::SplitCostCenterIdRole);
0305         }
0306     }
0307 
0308     return rc;
0309 }
0310 
0311 bool NewTransactionEditor::Private::isIncomeExpense(const QString& categoryId) const
0312 {
0313     if (!categoryId.isEmpty()) {
0314         MyMoneyAccount category = MyMoneyFile::instance()->account(categoryId);
0315         return category.isIncomeExpense();
0316     }
0317     return false;
0318 }
0319 
0320 bool NewTransactionEditor::Private::isIncomeExpense(const QModelIndex& idx) const
0321 {
0322     return isIncomeExpense(idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0323 }
0324 
0325 void NewTransactionEditor::Private::accountChanged(const QString& id)
0326 {
0327     m_account = MyMoneyFile::instance()->accountsModel()->itemById(id);
0328     m_split.setAccountId(id);
0329 
0330     m_transaction.setCommodity(m_account.currencyId());
0331 
0332     // in case we have a single split, we set the categoryCombo again
0333     // so that a possible foreign currency is also taken care of.
0334     if (splitModel.rowCount() == 1) {
0335         ui->categoryCombo->setSelected(splitModel.index(0, 0).data(eMyMoney::Model::SplitAccountIdRole).toString());
0336     }
0337 
0338     updateWidgetAccess();
0339 }
0340 
0341 bool NewTransactionEditor::Private::categoryChanged(const QString& accountId)
0342 {
0343     bool rc = true;
0344     if (splitModel.rowCount() <= 1) {
0345         if (!accountId.isEmpty()) {
0346             try {
0347                 MyMoneyAccount category = MyMoneyFile::instance()->account(accountId);
0348                 const bool isIncomeExpense = category.isIncomeExpense();
0349                 ui->costCenterCombo->setEnabled(isIncomeExpense);
0350                 ui->costCenterLabel->setEnabled(isIncomeExpense);
0351                 costCenterRequired = category.isCostCenterRequired();
0352 
0353                 // make sure we have a split in the model
0354                 if (splitModel.rowCount() == 0) {
0355                     // add a first split with account assigned
0356                     MyMoneySplit s;
0357                     s.setAccountId(accountId);
0358                     // the following call does not assign a split ID
0359                     // this will be done in SplitModel::addSplitsToTransaction()
0360                     splitModel.appendSplit(s);
0361                 }
0362 
0363                 const auto index = splitModel.index(0, 0);
0364                 splitModel.setData(index, accountId, eMyMoney::Model::SplitAccountIdRole);
0365 
0366                 rc &= costCenterChanged(ui->costCenterCombo->currentIndex());
0367                 rc &= postdateChanged(ui->dateEdit->date());
0368                 payeeChanged(ui->payeeEdit->currentIndex());
0369 
0370                 // extract the categories currency
0371                 const auto accountIdx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
0372                 const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0373                 const auto currency = MyMoneyFile::instance()->currenciesModel()->itemById(currencyId);
0374 
0375                 // in case the commodity changes, we need to update the shares part
0376                 if (currency.id() != ui->creditDebitEdit->sharesCommodity().id()) {
0377                     ui->creditDebitEdit->setSharesCommodity(currency);
0378                     auto sharesAmount = ui->creditDebitEdit->value();
0379                     ui->creditDebitEdit->setShares(sharesAmount);
0380                     // switch to value display so that we show the transaction commodity
0381                     // for single currency data entry this does not have an effect
0382                     ui->creditDebitEdit->setDisplayState(MultiCurrencyEdit::DisplayValue);
0383 
0384                     if (!sharesAmount.isZero()) {
0385                         KCurrencyCalculator::updateConversion(ui->creditDebitEdit, ui->dateEdit->date());
0386                     }
0387                 }
0388 
0389                 splitModel.setData(index, QVariant::fromValue<MyMoneyMoney>(-ui->creditDebitEdit->value()), eMyMoney::Model::SplitValueRole);
0390                 splitModel.setData(index, QVariant::fromValue<MyMoneyMoney>(-ui->creditDebitEdit->shares()), eMyMoney::Model::SplitSharesRole);
0391 
0392                 updateVAT(ValueUnchanged);
0393 
0394                 keepCategoryAmount = false;
0395 
0396             } catch (MyMoneyException&) {
0397                 qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO;
0398             }
0399         } else {
0400             splitModel.unload();
0401         }
0402     }
0403     ui->tagContainer->setEnabled(splitModel.rowCount() == 1);
0404     return rc;
0405 }
0406 
0407 bool NewTransactionEditor::Private::numberChanged(const QString& newNumber)
0408 {
0409     bool rc = true; // number did change
0410     WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction."));
0411     if (!newNumber.isEmpty()) {
0412         auto model = MyMoneyFile::instance()->journalModel();
0413         const QModelIndexList list = model->match(model->index(0, 0), eMyMoney::Model::SplitNumberRole,
0414                                      QVariant(newNumber),
0415                                      -1,                         // all splits
0416                                      Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
0417         for (const auto& idx : list) {
0418             if (idx.data(eMyMoney::Model::SplitAccountIdRole).toString() == m_account.id()
0419                 && idx.data(eMyMoney::Model::JournalTransactionIdRole).toString().compare(m_transaction.id())) {
0420                 WidgetHintFrame::show(ui->numberEdit, i18n("The check number <b>%1</b> has already been used in this account.", newNumber));
0421                 rc = false;
0422                 break;
0423             }
0424         }
0425     }
0426     return rc;
0427 }
0428 
0429 bool NewTransactionEditor::Private::amountChanged()
0430 {
0431     bool rc = true;
0432     if (ui->creditDebitEdit->haveValue() && (splitModel.rowCount() <= 1)) {
0433         try {
0434             if (splitModel.rowCount() == 1) {
0435                 const QModelIndex index = splitModel.index(0, 0);
0436 
0437                 if (!keepCategoryAmount) {
0438                     // check if there is a change in the values other than simply reverting the sign
0439                     // and get an updated price in that case
0440                     if ((index.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>() != ui->creditDebitEdit->shares())
0441                         || (index.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>() != ui->creditDebitEdit->value())) {
0442                         KCurrencyCalculator::updateConversion(ui->creditDebitEdit, ui->dateEdit->date());
0443                     }
0444 
0445                     splitModel.setData(index, QVariant::fromValue<MyMoneyMoney>(-ui->creditDebitEdit->shares()), eMyMoney::Model::SplitSharesRole);
0446                 }
0447                 splitModel.setData(index, QVariant::fromValue<MyMoneyMoney>(-ui->creditDebitEdit->value()), eMyMoney::Model::SplitValueRole);
0448             }
0449 
0450         } catch (MyMoneyException&) {
0451             rc = false;
0452             qDebug() << "Ooops: something went wrong in" << Q_FUNC_INFO;
0453         }
0454     } else {
0455         /// @todo ask what to do: if the rest of the splits is the same amount we could simply reverse the sign
0456         /// of all splits, otherwise we could ask if the user wants to start the split editor or anything else.
0457     }
0458     updateVAT(ValueChanged);
0459     return rc;
0460 }
0461 
0462 void NewTransactionEditor::Private::payeeChanged(int payeeIndex)
0463 {
0464     const auto payeeId = payeesModel->index(payeeIndex, 0).data(eMyMoney::Model::IdRole).toString();
0465     const AutoFillMethod autoFillMethod = static_cast<AutoFillMethod>(KMyMoneySettings::autoFillTransaction());
0466 
0467     // we have a new payee assigned to this transaction.
0468     // in case there is no category assigned, no value entered and no
0469     // memo available, we search for the last transaction of this payee
0470     // in the selected account.
0471     if (m_transaction.id().isEmpty() && (splitModel.rowCount() == 0) && !ui->creditDebitEdit->haveValue() && ui->memoEdit->toPlainText().isEmpty()
0472         && !m_account.id().isEmpty() && (autoFillMethod != AutoFillMethod::NoAutoFill)) {
0473         // if we got here, we have to autofill
0474         autoFillTransaction(payeeId);
0475     }
0476     // copy payee information to second split if there are only two splits
0477     if (splitModel.rowCount() == 1) {
0478         const auto idx = splitModel.index(0, 0);
0479         splitModel.setData(idx, payeeId, eMyMoney::Model::SplitPayeeIdRole);
0480     }
0481 }
0482 
0483 void NewTransactionEditor::Private::autoFillTransaction(const QString& payeeId)
0484 {
0485     struct uniqTransaction {
0486         QString journalEntryId;
0487         MyMoneyTransaction transaction;
0488         int matches;
0489     };
0490 
0491     /**
0492      * Sum up all splits for the given account in the transaction
0493      */
0494     auto shares = [&](const MyMoneyTransaction& t, const QString& accountId) {
0495         MyMoneyMoney result;
0496         for (const auto& split : t.splits()) {
0497             if (split.accountId() == accountId) {
0498                 result += split.shares();
0499             }
0500         }
0501         return result;
0502     };
0503 
0504     const auto journalModel = MyMoneyFile::instance()->journalModel();
0505     MyMoneyTransactionFilter filter(m_account.id());
0506     filter.addPayee(payeeId);
0507     QStringList journalEntryIds(journalModel->journalEntryIds(filter));
0508 
0509     if (!journalEntryIds.empty()) {
0510         const AutoFillMethod autoFillMethod = static_cast<AutoFillMethod>(KMyMoneySettings::autoFillTransaction());
0511         // ok, we found at least one previous transaction. now we clear out
0512         // what we have collected so far and add those splits from
0513         // the previous transaction.
0514         QMap<QString, struct uniqTransaction> uniqList;
0515 
0516         // collect the journal entries and see if we have any duplicates
0517         for (const auto& journalEntryId : journalEntryIds) {
0518             const auto journalEntry = journalModel->itemById(journalEntryId);
0519             int cnt = 0;
0520             QMap<QString, struct uniqTransaction>::iterator it_u;
0521             do {
0522                 QString ukey = QString("%1-%2").arg(journalEntry.transaction().accountSignature()).arg(cnt);
0523                 it_u = uniqList.find(ukey);
0524                 if (it_u == uniqList.end()) {
0525                     uniqList[ukey].journalEntryId = journalEntryId;
0526                     uniqList[ukey].transaction = journalEntry.transaction();
0527                     uniqList[ukey].matches = 1;
0528 
0529                 } else if (autoFillMethod == AutoFillMethod::AutoFillWithClosestInValue) {
0530                     // we already have a transaction with this signature. we must
0531                     // now check, if we should really treat it as a duplicate according
0532                     // to the value comparison delta.
0533                     MyMoneyMoney s1 = shares(((*it_u).transaction), m_account.id());
0534                     MyMoneyMoney s2 = shares(journalEntry.transaction(), m_account.id());
0535                     if (s2.abs() > s1.abs()) {
0536                         MyMoneyMoney t(s1);
0537                         s1 = s2;
0538                         s2 = t;
0539                     }
0540                     MyMoneyMoney diff;
0541                     if (s2.isZero()) {
0542                         diff = s1.abs();
0543                     } else {
0544                         diff = ((s1 - s2) / s2).convert(10000);
0545                     }
0546                     if (diff.isPositive() && diff <= MyMoneyMoney(KMyMoneySettings::autoFillDifference(), 100)) {
0547                         uniqList[ukey].journalEntryId = journalEntryId;
0548                         uniqList[ukey].transaction = journalEntry.transaction();
0549                         break; // end while loop
0550                     }
0551                 } else if (autoFillMethod == AutoFillMethod::AutoFillWithMostOftenUsed) {
0552                     uniqList[ukey].journalEntryId = journalEntryId;
0553                     uniqList[ukey].transaction = journalEntry.transaction();
0554                     (*it_u).matches++;
0555                     break; // end while loop
0556                 }
0557                 ++cnt;
0558             } while (it_u != uniqList.end());
0559         }
0560 
0561         QString journalEntryId;
0562         if (autoFillMethod != AutoFillMethod::AutoFillWithMostOftenUsed) {
0563             QPointer<KTransactionSelectDlg> dlg = new KTransactionSelectDlg();
0564             dlg->setWindowTitle(i18nc("@title:window Autofill selection dialog", "Select autofill transaction"));
0565 
0566             QMap<QString, struct uniqTransaction>::const_iterator it_u;
0567             for (it_u = uniqList.constBegin(); it_u != uniqList.constEnd(); ++it_u) {
0568                 dlg->addTransaction((*it_u).journalEntryId);
0569             }
0570 
0571             // Sort by
0572             // - ascending post date
0573             // - descending reconciliation state
0574             // - descending value
0575             dlg->ledgerView()->setSortOrder(LedgerSortOrder("1,-9,-4"));
0576             dlg->ledgerView()->selectMostRecentTransaction();
0577             if (dlg->exec() == QDialog::Accepted) {
0578                 journalEntryId = dlg->journalEntryId();
0579             }
0580         } else {
0581             int maxCnt = 0;
0582             QMap<QString, struct uniqTransaction>::const_iterator it_u;
0583             for (it_u = uniqList.constBegin(); it_u != uniqList.constEnd(); ++it_u) {
0584                 if ((*it_u).matches > maxCnt) {
0585                     journalEntryId = (*it_u).journalEntryId;
0586                     maxCnt = (*it_u).matches;
0587                 }
0588             }
0589         }
0590 
0591         if (!journalEntryId.isEmpty()) {
0592             // keep data we don't want to change by loading
0593             const auto postDate = ui->dateEdit->date();
0594             const auto number = ui->numberEdit->text();
0595             // now load the existing transaction into the editor
0596             auto index = MyMoneyFile::instance()->journalModel()->indexById(journalEntryId);
0597             loadTransaction(index);
0598 
0599             // restore data we don't want to change by loading
0600             ui->dateEdit->setDate(postDate);
0601 
0602             if (ui->numberEdit->isVisible() && !number.isEmpty()) {
0603                 ui->numberEdit->setText(number);
0604             } else if (!m_split.number().isEmpty()) {
0605                 ui->numberEdit->setText(KMyMoneyUtils::nextFreeCheckNumber(m_account));
0606             }
0607 
0608             // make sure to really create a new transaction
0609             m_transaction.clearId();
0610             ui->statusCombo->setCurrentIndex(static_cast<int>(eMyMoney::Split::State::NotReconciled));
0611             m_split = prepareSplit(m_split);
0612 
0613             splitModel.resetAllSplitIds();
0614             for (int row = 0; row < splitModel.rowCount(); ++row) {
0615                 const auto idx = splitModel.index(row, 0);
0616                 splitModel.setData(idx, QVariant::fromValue(eMyMoney::Split::State::NotReconciled), eMyMoney::Model::SplitReconcileFlagRole);
0617                 splitModel.setData(idx, QDate(), eMyMoney::Model::SplitReconcileDateRole);
0618                 splitModel.setData(idx, QString(), eMyMoney::Model::SplitBankIdRole);
0619                 splitModel.setData(idx, QString(), eMyMoney::Model::SplitMemoRole);
0620 
0621                 if (needClearSplitAction(idx.data(eMyMoney::Model::SplitActionRole).toString())) {
0622                     splitModel.setData(idx, QString(), eMyMoney::Model::SplitActionRole);
0623                 }
0624             }
0625         }
0626     }
0627 
0628     /// @todo maybe set focus to next tab widget
0629 }
0630 
0631 MyMoneySplit NewTransactionEditor::Private::prepareSplit(const MyMoneySplit& sp)
0632 {
0633     auto split(sp);
0634     split.setReconcileDate(QDate());
0635     split.setBankID(QString());
0636     // older versions of KMyMoney used to set the action
0637     // we don't need this anymore
0638     if (needClearSplitAction(split.action())) {
0639         split.setAction(QString());
0640     }
0641     split.setNumber(QString());
0642     if (!KMyMoneySettings::autoFillUseMemos()) {
0643         split.setMemo(QString());
0644     }
0645 
0646     return split;
0647 }
0648 
0649 bool NewTransactionEditor::Private::needClearSplitAction(const QString& action) const
0650 {
0651     return (action != MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization) && action != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
0652 }
0653 
0654 bool NewTransactionEditor::Private::tagsChanged(const QStringList& ids)
0655 {
0656     if (splitModel.rowCount() == 1) {
0657         const auto idx = splitModel.index(0, 0);
0658         splitModel.setData(idx, ids, eMyMoney::Model::SplitTagIdRole);
0659     }
0660     return true;
0661 }
0662 
0663 MyMoneyMoney NewTransactionEditor::Private::splitsSum() const
0664 {
0665     const auto rows = splitModel.rowCount();
0666     MyMoneyMoney value;
0667     for(int row = 0; row < rows; ++row) {
0668         const auto idx = splitModel.index(row, 0);
0669         value += idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>();
0670     }
0671     return value;
0672 }
0673 
0674 int NewTransactionEditor::Private::editSplits()
0675 {
0676     const auto transactionFactor(ui->creditDebitEdit->value().isNegative() ? MyMoneyMoney::ONE : MyMoneyMoney::MINUS_ONE);
0677 
0678     SplitModel dlgSplitModel(q, nullptr, splitModel);
0679 
0680     // create an empty split at the end
0681     // used to create new splits, but only
0682     // when not in read-only mode
0683     if (!q->isReadOnly())
0684         dlgSplitModel.appendEmptySplit();
0685 
0686     // in case the transaction does only have a single split (the
0687     // one referencing the account) we keep a possible filled memo
0688     // and add it to the empty split.
0689     if ((dlgSplitModel.rowCount() == 1) && (!ui->memoEdit->toPlainText().isEmpty())) {
0690         const auto idx = dlgSplitModel.index(0, 0);
0691         dlgSplitModel.setData(idx, ui->memoEdit->toPlainText(), eMyMoney::Model::SplitMemoRole);
0692     }
0693     auto commodityId = m_transaction.commodity();
0694     if (commodityId.isEmpty())
0695         commodityId = m_account.currencyId();
0696     dlgSplitModel.setTransactionCommodity(commodityId);
0697     const auto commodity = MyMoneyFile::instance()->security(commodityId);
0698 
0699     QPointer<SplitDialog> splitDialog = new SplitDialog(commodity, -(q->transactionAmount()), m_account.fraction(), transactionFactor, q);
0700     const auto payeeId = payeesModel->index(ui->payeeEdit->currentIndex(), 0).data(eMyMoney::Model::IdRole).toString();
0701     splitDialog->setTransactionPayeeId(payeeId);
0702     splitDialog->setModel(&dlgSplitModel);
0703     splitDialog->setReadOnly(q->isReadOnly());
0704 
0705     int rc = splitDialog->exec();
0706 
0707     if (splitDialog && (rc == QDialog::Accepted)) {
0708         // remove that empty split again before we update the splits
0709         // no need to check for presence, removeEmptySplit() does that
0710         dlgSplitModel.removeEmptySplit();
0711 
0712         // copy the splits model contents
0713         splitModel = dlgSplitModel;
0714 
0715         // update the transaction amount
0716         ui->creditDebitEdit->setSharesCommodity(ui->creditDebitEdit->valueCommodity());
0717         ui->creditDebitEdit->setValue(-splitDialog->transactionAmount());
0718         auto amountShares = -splitDialog->transactionAmount();
0719 
0720         // the price might have been changed, so we have to update our copy
0721         // but only if there is one counter split
0722         if (splitModel.rowCount() == 1) {
0723             const auto idx = splitModel.index(0, 0);
0724 
0725             // use the shares based on the second split
0726             amountShares = -(idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0727 
0728             // adjust the commodity for the shares
0729             const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0730             const auto accountIdx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
0731             const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0732             const auto currency = MyMoneyFile::instance()->currenciesModel()->itemById(currencyId);
0733             ui->creditDebitEdit->setSharesCommodity(currency);
0734         }
0735         ui->creditDebitEdit->setShares(amountShares);
0736 
0737         updateWidgetState();
0738 
0739         QWidget* next = ui->tagContainer->tagCombo();
0740         if (ui->costCenterCombo->isEnabled()) {
0741             next = ui->costCenterCombo;
0742         }
0743         next->setFocus();
0744     }
0745 
0746     if (splitDialog) {
0747         splitDialog->deleteLater();
0748     }
0749 
0750     return rc;
0751 }
0752 
0753 MyMoneyMoney NewTransactionEditor::Private::removeVatSplit()
0754 {
0755     const auto rows = splitModel.rowCount();
0756     if (rows != 2)
0757         return ui->creditDebitEdit->value();
0758 
0759     QModelIndex netSplitIdx;
0760     QModelIndex taxSplitIdx;
0761     bool netValue(false);
0762 
0763     for (int row = 0; row < rows; ++row) {
0764         const auto idx = splitModel.index(row, 0);
0765         const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0766         const auto account = MyMoneyFile::instance()->accountsModel()->itemById(accountId);
0767         // in case of failure, we simply stop processing
0768         if (account.id().isEmpty()) {
0769             return ui->creditDebitEdit->value();
0770         }
0771         if (!account.value(QLatin1String("VatAccount")).isEmpty()) {
0772             netValue = (account.value(QLatin1String("VatAmount")).toLower() == QLatin1String("net"));
0773             netSplitIdx = idx;
0774         } else if (!account.value(QLatin1String("VatRate")).isEmpty()) {
0775             taxSplitIdx = idx;
0776         }
0777     }
0778 
0779     // return if not all splits are setup
0780     if (!(taxSplitIdx.isValid() && netSplitIdx.isValid())) {
0781         return ui->creditDebitEdit->value();
0782     }
0783 
0784     MyMoneyMoney amount;
0785     // reduce the splits
0786     if (netValue) {
0787         amount = -(netSplitIdx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0788     } else {
0789         amount = -(netSplitIdx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>()
0790                    + taxSplitIdx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0791     }
0792 
0793     // remove the tax split
0794     splitModel.removeRow(netSplitIdx.row());
0795 
0796     return amount;
0797 }
0798 
0799 void NewTransactionEditor::Private::updateVAT(TaxValueChange amountChanged)
0800 {
0801     if (inUpdateVat) {
0802         return;
0803     }
0804 
0805     struct cleanupHelper {
0806         cleanupHelper(bool* lockVariable)
0807             : m_lockVariable(lockVariable)
0808         {
0809             *lockVariable = true;
0810         }
0811         ~cleanupHelper()
0812         {
0813             *m_lockVariable = false;
0814         }
0815         bool* m_lockVariable;
0816     } cleanupHelper(&inUpdateVat);
0817 
0818     const auto categoryId = ui->categoryCombo->getSelected();
0819 
0820     auto taxCategoryId = [&]() {
0821         if (categoryId.isEmpty()) {
0822             return QString();
0823         }
0824         const auto category = MyMoneyFile::instance()->account(categoryId);
0825         return category.value(QLatin1String("VatAccount"));
0826     };
0827 
0828     // if auto vat assignment for this account is turned off
0829     // we don't care about taxes
0830     if (m_account.value(QLatin1String("NoVat")).toLower() == QLatin1String("yes"))
0831         return;
0832 
0833     // more splits than category and tax are not supported
0834     if (splitModel.rowCount() > 2)
0835         return;
0836 
0837     // in order to do anything, we need an amount
0838     MyMoneyMoney amount, newAmount;
0839     amount = ui->creditDebitEdit->value();
0840     if (amount.isZero())
0841         return;
0842 
0843     MyMoneyAccount category;
0844 
0845     // If the transaction has a tax and a category split, remove the tax split
0846     if (splitModel.rowCount() == 2) {
0847         newAmount = removeVatSplit();
0848         if (splitModel.rowCount() == 2) // not removed?
0849             return;
0850 
0851         // now we have a single split with a category and check if the
0852         // value has changed and we need to update that split
0853         if (amountChanged == ValueChanged) {
0854             categoryChanged(categoryId);
0855         }
0856     } else {
0857         newAmount = amount;
0858     }
0859 
0860     const auto taxId = taxCategoryId();
0861     if (taxId.isEmpty())
0862         return;
0863 
0864     // seems we have everything we need
0865     if (amountChanged == ValueChanged)
0866         newAmount = amount;
0867 
0868     if (splitModel.rowCount() != 1)
0869         return;
0870 
0871     auto t = q->transaction();
0872     t.setCommodity(m_transaction.commodity());
0873     MyMoneyFile::instance()->updateVAT(t);
0874 
0875     // clear current splits and add them again
0876     splitModel.unload();
0877     for (const auto& split : t.splits()) {
0878         if ((split.accountId() == taxId) || split.accountId() == categoryId) {
0879             splitModel.appendSplit(split);
0880         }
0881     }
0882 }
0883 
0884 void NewTransactionEditor::Private::setupTabOrder()
0885 {
0886     const auto defaultTabOrder = QStringList{
0887         QLatin1String("accountCombo"),
0888         QLatin1String("dateEdit"),
0889         QLatin1String("creditDebitEdit"),
0890         QLatin1String("payeeEdit"),
0891         QLatin1String("numberEdit"),
0892         QLatin1String("categoryCombo"),
0893         QLatin1String("costCenterCombo"),
0894         QLatin1String("tagContainer"),
0895         QLatin1String("statusCombo"),
0896         QLatin1String("memoEdit"),
0897         QLatin1String("enterButton"),
0898         QLatin1String("cancelButton"),
0899     };
0900     q->setProperty("kmm_defaulttaborder", defaultTabOrder);
0901     q->setProperty("kmm_currenttaborder", q->tabOrder(QLatin1String("stdTransactionEditor"), defaultTabOrder));
0902 
0903     q->setupTabOrder(q->property("kmm_currenttaborder").toStringList());
0904 }
0905 
0906 void NewTransactionEditor::Private::defaultCategoryAssignment()
0907 {
0908     if (splitModel.rowCount() == 0) {
0909         const auto payeeIdx = payeesModel->index(ui->payeeEdit->currentIndex(), 0);
0910         const auto defaultAccount = payeeIdx.data(eMyMoney::Model::PayeeDefaultAccountRole).toString();
0911         if (!defaultAccount.isEmpty()) {
0912             categoryChanged(defaultAccount);
0913         }
0914     }
0915 }
0916 
0917 /**
0918  * @note @a idx must be the base model index
0919  */
0920 void NewTransactionEditor::Private::loadTransaction(QModelIndex idx)
0921 {
0922     // we block sending out signals for the account and category combo here
0923     // to avoid calling NewTransactionEditorPrivate::categoryChanged which
0924     // does not work properly when loading the editor
0925     QSignalBlocker accountBlocker(ui->accountCombo->lineEdit());
0926     ui->accountCombo->clearEditText();
0927     QSignalBlocker categoryBlocker(ui->categoryCombo->lineEdit());
0928     ui->categoryCombo->clearEditText();
0929 
0930     // find which item has this id and set is as the current item
0931     const auto selectedSplitRow = idx.row();
0932 
0933     // keep a copy of the transaction and split
0934     m_transaction = MyMoneyFile::instance()->journalModel()->itemByIndex(idx).transaction();
0935     m_split = MyMoneyFile::instance()->journalModel()->itemByIndex(idx).split();
0936     const auto list = idx.model()->match(idx.model()->index(0, 0),
0937                                          eMyMoney::Model::JournalTransactionIdRole,
0938                                          idx.data(eMyMoney::Model::JournalTransactionIdRole),
0939                                          -1, // all splits
0940                                          Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
0941 
0942     // make sure the commodity is the one of the current account
0943     // in case we have exactly two splits. This is a precondition
0944     // used by the transaction editor to work properly.
0945     auto amountValue = m_split.value();
0946     if (m_transaction.splitCount() == 2) {
0947         amountValue = m_split.shares();
0948         m_split.setValue(amountValue);
0949     }
0950 
0951     // preset the value to be used for the amount widget
0952     auto amountShares = m_split.shares();
0953 
0954     // block the signals sent out from the model here so that
0955     // connected widgets don't overwrite the values we just loaded
0956     // because they are not yet set (d->ui->creditDebitEdit)
0957     QSignalBlocker blocker(splitModel);
0958 
0959     for (const auto& splitIdx : list) {
0960         if (selectedSplitRow == splitIdx.row()) {
0961             ui->dateEdit->setDate(splitIdx.data(eMyMoney::Model::TransactionPostDateRole).toDate());
0962 
0963             const auto payeeId = splitIdx.data(eMyMoney::Model::SplitPayeeIdRole).toString();
0964             const QModelIndex payeeIdx = MyMoneyFile::instance()->payeesModel()->indexById(payeeId);
0965             if (payeeIdx.isValid()) {
0966                 ui->payeeEdit->setCurrentIndex(MyMoneyFile::baseModel()->mapFromBaseSource(payeesModel, payeeIdx).row());
0967             } else {
0968                 ui->payeeEdit->setCurrentIndex(0);
0969             }
0970 
0971             ui->memoEdit->clear();
0972             ui->memoEdit->insertPlainText(splitIdx.data(eMyMoney::Model::SplitMemoRole).toString());
0973             ui->memoEdit->moveCursor(QTextCursor::Start);
0974             ui->memoEdit->ensureCursorVisible();
0975 
0976             ui->numberEdit->setText(splitIdx.data(eMyMoney::Model::SplitNumberRole).toString());
0977             ui->statusCombo->setCurrentIndex(splitIdx.data(eMyMoney::Model::SplitReconcileFlagRole).toInt());
0978         } else {
0979             splitModel.appendSplit(MyMoneyFile::instance()->journalModel()->itemByIndex(splitIdx).split());
0980 
0981             if (splitIdx.data(eMyMoney::Model::TransactionSplitCountRole) == 2) {
0982                 // force the value of the second split to be the same as for the first
0983                 idx = splitModel.index(0, 0);
0984                 splitModel.setData(idx, QVariant::fromValue<MyMoneyMoney>(-amountValue), eMyMoney::Model::SplitValueRole);
0985 
0986                 // use the shares based on the second split
0987                 amountShares = -(splitIdx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0988 
0989                 // adjust the commodity for the shares
0990                 const auto accountId = splitIdx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0991                 const auto accountIdx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
0992                 const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0993                 const auto currency = MyMoneyFile::instance()->currenciesModel()->itemById(currencyId);
0994                 ui->creditDebitEdit->setSharesCommodity(currency);
0995             }
0996         }
0997     }
0998     m_transaction.setCommodity(m_account.currencyId());
0999 
1000     adjustTagIdList();
1001     ui->tagContainer->loadTags(m_split.tagIdList());
1002 
1003     // then setup the amount widget and update the state
1004     // of all other widgets
1005     ui->creditDebitEdit->setValue(amountValue);
1006     ui->creditDebitEdit->setShares(amountShares);
1007 
1008     updateWidgetState();
1009     m_splitHelper->updateWidget();
1010 }
1011 
1012 NewTransactionEditor::NewTransactionEditor(QWidget* parent, const QString& accountId)
1013     : TransactionEditorBase(parent, accountId)
1014     , d(new Private(this))
1015 {
1016     auto const file = MyMoneyFile::instance();
1017     auto const model = file->accountsModel();
1018     // extract account information from model
1019     const auto index = model->indexById(accountId);
1020     d->m_account = model->itemByIndex(index);
1021 
1022     d->ui->setupUi(this);
1023 
1024     // default is to hide the account selection combobox
1025     setShowAccountCombo(false);
1026 
1027     d->setupTabOrder();
1028 
1029     // determine order of credit and debit edit widgets
1030     // based on their visual order in the ledger
1031     int creditColumn = JournalModel::Column::Payment;
1032     int debitColumn = JournalModel::Column::Deposit;
1033 
1034     QWidget* w(this);
1035     do {
1036         w = w->parentWidget();
1037         const auto view = qobject_cast<const QTableView*>(w);
1038         if (view) {
1039             creditColumn = view->horizontalHeader()->visualIndex(creditColumn);
1040             debitColumn = view->horizontalHeader()->visualIndex(debitColumn);
1041             break;
1042         }
1043     } while (w);
1044 
1045     // in case they are in the opposite order, we swap the edit widgets
1046     if (debitColumn < creditColumn) {
1047         d->ui->creditDebitEdit->swapCreditDebit();
1048     }
1049 
1050     d->m_splitHelper = new KMyMoneyAccountComboSplitHelper(d->ui->categoryCombo, &d->splitModel);
1051     connect(d->m_splitHelper, &KMyMoneyAccountComboSplitHelper::accountComboEnabled, d->ui->costCenterCombo, &QComboBox::setEnabled);
1052     connect(d->m_splitHelper, &KMyMoneyAccountComboSplitHelper::accountComboEnabled, d->ui->costCenterLabel, &QComboBox::setEnabled);
1053     connect(d->m_splitHelper, &KMyMoneyAccountComboSplitHelper::accountComboEnabled, this, &NewTransactionEditor::categorySelectionChanged);
1054 
1055     d->accountsModel->addAccountGroup(QVector<eMyMoney::Account::Type>{
1056         eMyMoney::Account::Type::Asset,
1057         eMyMoney::Account::Type::Liability,
1058         eMyMoney::Account::Type::Equity,
1059     });
1060     d->accountsModel->setHideEquityAccounts(false);
1061     d->accountsModel->setHideZeroBalancedEquityAccounts(false);
1062     d->accountsModel->setHideZeroBalancedAccounts(false);
1063     d->accountsModel->setShowAllEntries(KMyMoneySettings::showAllAccounts());
1064     d->accountsModel->setSourceModel(model);
1065     d->accountsModel->sort(AccountsModel::Column::AccountName);
1066     d->ui->accountCombo->setModel(d->accountsModel);
1067 
1068     d->categoriesModel->addAccountGroup(QVector<eMyMoney::Account::Type>{
1069         eMyMoney::Account::Type::Asset,
1070         eMyMoney::Account::Type::Liability,
1071         eMyMoney::Account::Type::Income,
1072         eMyMoney::Account::Type::Expense,
1073         eMyMoney::Account::Type::Equity,
1074     });
1075     d->categoriesModel->setHideEquityAccounts(false);
1076     d->categoriesModel->setShowAllEntries(KMyMoneySettings::showAllAccounts());
1077     d->categoriesModel->setSourceModel(model);
1078     d->categoriesModel->sort(AccountsModel::Column::AccountName);
1079     d->ui->categoryCombo->setModel(d->categoriesModel);
1080 
1081     d->ui->tagContainer->setModel(file->tagsModel()->modelWithEmptyItem());
1082 
1083     d->costCenterModel->setSortRole(Qt::DisplayRole);
1084     d->costCenterModel->setSourceModel(file->costCenterModel()->modelWithEmptyItem());
1085     d->costCenterModel->setSortLocaleAware(true);
1086     d->costCenterModel->sort(0);
1087 
1088     d->ui->costCenterCombo->setEditable(true);
1089     d->ui->costCenterCombo->setModel(d->costCenterModel);
1090     d->ui->costCenterCombo->setModelColumn(0);
1091     d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains);
1092 
1093     d->payeesModel->setSortRole(Qt::DisplayRole);
1094     d->payeesModel->setSourceModel(file->payeesModel()->modelWithEmptyItem());
1095     d->payeesModel->setSortLocaleAware(true);
1096     d->payeesModel->sort(0);
1097 
1098     d->ui->payeeEdit->setEditable(true);
1099     d->ui->payeeEdit->lineEdit()->setClearButtonEnabled(true);
1100     d->ui->payeeEdit->setModel(d->payeesModel);
1101     d->ui->payeeEdit->setModelColumn(0);
1102     d->ui->payeeEdit->completer()->setCompletionMode(QCompleter::PopupCompletion);
1103     d->ui->payeeEdit->completer()->setFilterMode(Qt::MatchContains);
1104 
1105     // make sure that there is no selection left in the background
1106     // in case there is no text in the edit field
1107     connect(d->ui->payeeEdit->lineEdit(), &QLineEdit::textEdited, [&](const QString& txt) {
1108         if (txt.isEmpty()) {
1109             d->ui->payeeEdit->setCurrentIndex(-1);
1110         }
1111     });
1112 
1113     connect(d->ui->categoryCombo->lineEdit(), &QLineEdit::textEdited, [&](const QString& txt) {
1114         if (txt.isEmpty()) {
1115             d->ui->categoryCombo->setSelected(QString());
1116         }
1117     });
1118     d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK));
1119     d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel));
1120 
1121     d->ui->statusCombo->setModel(MyMoneyFile::instance()->statusModel());
1122 
1123     d->ui->creditDebitEdit->setAllowEmpty(true);
1124 
1125     d->frameCollection = new WidgetHintFrameCollection(this);
1126     d->frameCollection->addFrame(new WidgetHintFrame(d->ui->dateEdit));
1127     d->frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo));
1128     d->frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning));
1129     d->frameCollection->addWidget(d->ui->enterButton);
1130 
1131     connect(d->ui->numberEdit, &QLineEdit::textChanged, this, [&](const QString& newNumber) {
1132         d->numberChanged(newNumber);
1133     });
1134 
1135     connect(d->ui->costCenterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int costCenterIndex) {
1136         d->costCenterChanged(costCenterIndex);
1137     });
1138 
1139     connect(d->ui->accountCombo, &KMyMoneyAccountCombo::accountSelected, this, [&](const QString& id) {
1140         d->accountChanged(id);
1141     });
1142     connect(d->ui->categoryCombo, &KMyMoneyAccountCombo::accountSelected, this, [&](const QString& id) {
1143         d->categoryChanged(id);
1144     });
1145 
1146     connect(d->ui->categoryCombo, &KMyMoneyAccountCombo::splitDialogRequest, this, [&]() {
1147         d->editSplits();
1148     });
1149 
1150     connect(d->ui->dateEdit, &KMyMoneyDateEdit::dateValidityChanged, this, [&](const QDate& date) {
1151         d->postdateChanged(date);
1152     });
1153 
1154     connect(d->ui->dateEdit, &KMyMoneyDateEdit::dateEntered, this, [&](const QDate& date) {
1155         d->postdateChanged(date);
1156         Q_EMIT postDateChanged(date);
1157     });
1158 
1159     connect(d->ui->creditDebitEdit, &CreditDebitEdit::amountChanged, this, [&]() {
1160         d->amountChanged();
1161     });
1162 
1163     connect(d->ui->payeeEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int payeeIndex) {
1164         d->payeeChanged(payeeIndex);
1165     });
1166 
1167     connect(d->ui->tagContainer, &KTagContainer::tagsChanged, this, [&](const QStringList& tagIds) {
1168         d->tagsChanged(tagIds);
1169     });
1170 
1171     connect(d->ui->cancelButton, &QToolButton::clicked, this, &NewTransactionEditor::reject);
1172     connect(d->ui->enterButton, &QToolButton::clicked, this, &NewTransactionEditor::acceptEdit);
1173 
1174     // handle some events in certain conditions different from default
1175     d->ui->payeeEdit->installEventFilter(this);
1176     d->ui->costCenterCombo->installEventFilter(this);
1177     d->ui->tagContainer->tagCombo()->installEventFilter(this);
1178     d->ui->categoryCombo->installEventFilter(this);
1179     d->ui->statusCombo->installEventFilter(this);
1180 
1181     setCancelButton(d->ui->cancelButton);
1182     setEnterButton(d->ui->enterButton);
1183 
1184     // force setup of filters
1185     slotSettingsChanged();
1186 }
1187 
1188 NewTransactionEditor::~NewTransactionEditor()
1189 {
1190 }
1191 
1192 void NewTransactionEditor::setAmountPlaceHolderText(const QAbstractItemModel* model)
1193 {
1194     d->ui->creditDebitEdit->setPlaceholderText(model->headerData(JournalModel::Column::Payment, Qt::Horizontal).toString(),
1195                                                model->headerData(JournalModel::Column::Deposit, Qt::Horizontal).toString());
1196 }
1197 
1198 void NewTransactionEditor::loadSchedule(const MyMoneySchedule& schedule)
1199 {
1200     if (schedule.transaction().splitCount() == 0) {
1201         // new schedule
1202         d->m_transaction = MyMoneyTransaction();
1203         d->m_transaction.setCommodity(MyMoneyFile::instance()->baseCurrency().id());
1204         d->m_split = MyMoneySplit();
1205         d->m_split.setAccountId(QString());
1206         const auto lastUsedPostDate = KMyMoneySettings::lastUsedPostDate();
1207         if (lastUsedPostDate.isValid()) {
1208             d->ui->dateEdit->setDate(lastUsedPostDate.date());
1209         } else {
1210             d->ui->dateEdit->setDate(QDate::currentDate());
1211         }
1212         QSignalBlocker accountBlocker(d->ui->accountCombo->lineEdit());
1213         d->ui->accountCombo->clearEditText();
1214         QSignalBlocker categoryBlocker(d->ui->categoryCombo->lineEdit());
1215         d->ui->categoryCombo->clearEditText();
1216         d->updateWidgetAccess();
1217 
1218         const auto commodity = MyMoneyFile::instance()->currency(d->m_transaction.commodity());
1219         d->ui->creditDebitEdit->setCommodity(commodity);
1220 
1221     } else {
1222         // existing schedule
1223         // since a scheduled transaction does not have an id, we assign it here so
1224         // that we can identify a transaction of an existing schedule. It will be
1225         // cleared when retrieving the transaction in the schedule editor via
1226         // KEditScheduleDlgPrivate::transaction()
1227         d->m_transaction = MyMoneyTransaction(schedule.id(), schedule.transaction());
1228         d->m_split = d->m_transaction.splits().first();
1229 
1230         const auto commodity = MyMoneyFile::instance()->currency(d->m_transaction.commodity());
1231         d->ui->creditDebitEdit->setCommodity(commodity);
1232         // update the commodity in case it was empty
1233         d->m_transaction.setCommodity(commodity.id());
1234 
1235         // make sure the commodity is the one of the current account
1236         // in case we have exactly two splits. This is a precondition
1237         // used by the transaction editor to work properly.
1238         auto amountValue = d->m_split.value();
1239         if (d->m_transaction.splitCount() == 2) {
1240             amountValue = d->m_split.shares();
1241             d->m_split.setValue(amountValue);
1242         }
1243 
1244         // preset the value to be used for the amount widget
1245         auto amountShares = d->m_split.shares();
1246 
1247         // block the signals sent out from the model here so that
1248         // connected widgets don't overwrite the values we just loaded
1249         // because they are not yet set (d->ui->creditDebitEdit)
1250         QSignalBlocker splitModelSignalBlocker(d->splitModel);
1251 
1252         // block the signals sent out from the payee edit widget so that
1253         // the autofill logic is not trigger when loading the schdule
1254         QSignalBlocker autoFillSignalBlocker(d->ui->payeeEdit);
1255 
1256         for (const auto& split : d->m_transaction.splits()) {
1257             if (split.id() == d->m_split.id()) {
1258                 d->ui->dateEdit->setDate(d->m_transaction.postDate());
1259 
1260                 const auto payeeId = split.payeeId();
1261                 const QModelIndex payeeIdx = MyMoneyFile::instance()->payeesModel()->indexById(payeeId);
1262                 if (payeeIdx.isValid()) {
1263                     d->ui->payeeEdit->setCurrentIndex(MyMoneyFile::baseModel()->mapFromBaseSource(d->payeesModel, payeeIdx).row());
1264                 } else {
1265                     d->ui->payeeEdit->setCurrentIndex(0);
1266                 }
1267 
1268                 d->ui->memoEdit->clear();
1269                 d->ui->memoEdit->insertPlainText(split.memo());
1270                 d->ui->memoEdit->moveCursor(QTextCursor::Start);
1271                 d->ui->memoEdit->ensureCursorVisible();
1272 
1273                 d->ui->numberEdit->setText(split.number());
1274                 d->ui->statusCombo->setCurrentIndex(static_cast<int>(split.reconcileFlag()));
1275             } else {
1276                 // we block sending out signals for the category combo here to avoid
1277                 // calling NewTransactionEditorPrivate::categoryChanged which does not
1278                 // work properly when loading the editor
1279                 QSignalBlocker categoryComboBlocker(d->ui->categoryCombo);
1280                 d->splitModel.appendSplit(split);
1281                 if (d->m_transaction.splitCount() == 2) {
1282                     // force the value of the second split to be the same as for the first
1283                     const auto idx = d->splitModel.index(0, 0);
1284                     d->splitModel.setData(idx, QVariant::fromValue<MyMoneyMoney>(-amountValue), eMyMoney::Model::SplitValueRole);
1285 
1286                     // use the shares based on the second split
1287                     amountShares = -split.shares();
1288 
1289                     // adjust the commodity for the shares
1290                     const auto accountId = split.accountId();
1291                     const auto accountIdx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
1292                     const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
1293                     const auto currency = MyMoneyFile::instance()->currenciesModel()->itemById(currencyId);
1294                     d->ui->creditDebitEdit->setSharesCommodity(currency);
1295                 }
1296             }
1297         }
1298         d->m_transaction.setCommodity(d->m_account.currencyId());
1299 
1300         d->adjustTagIdList();
1301 
1302         // then setup the amount widget and update the state
1303         // of all other widgets
1304         d->ui->creditDebitEdit->setValue(amountValue);
1305         d->ui->creditDebitEdit->setShares(amountShares);
1306         d->updateWidgetState();
1307         d->m_splitHelper->updateWidget();
1308     }
1309 }
1310 
1311 void NewTransactionEditor::loadTransaction(const QModelIndex& index)
1312 {
1313     // we may also get here during saving the transaction as
1314     // a callback from the model, but we can safely ignore it
1315     // same when we get called from the delegate's setEditorData()
1316     // method
1317     if (accepted() || !index.isValid() || d->loadedFromModel)
1318         return;
1319 
1320     d->loadedFromModel = true;
1321 
1322     auto idx = MyMoneyFile::baseModel()->mapToBaseSource(index);
1323     const auto commodity = MyMoneyFile::instance()->currency(d->m_account.currencyId());
1324     // set both the commodities to be the same here, in case of a two split transaction
1325     // and the other one being in a different commodity, we adjust that later on
1326     d->ui->creditDebitEdit->setCommodity(commodity);
1327 
1328     // we block sending out signals for the account and category combo here
1329     // to avoid calling NewTransactionEditorPrivate::categoryChanged which
1330     // does not work properly when loading the editor
1331     QSignalBlocker accountBlocker(d->ui->accountCombo->lineEdit());
1332     d->ui->accountCombo->clearEditText();
1333     QSignalBlocker categoryBlocker(d->ui->categoryCombo->lineEdit());
1334     d->ui->categoryCombo->clearEditText();
1335 
1336     if (idx.data(eMyMoney::Model::IdRole).toString().isEmpty()) {
1337         d->m_transaction = MyMoneyTransaction();
1338         d->m_transaction.setCommodity(commodity.id());
1339 
1340         d->m_split = MyMoneySplit();
1341         d->m_split.setAccountId(d->m_account.id());
1342         const auto lastUsedPostDate = KMyMoneySettings::lastUsedPostDate();
1343         if (lastUsedPostDate.isValid()) {
1344             d->ui->dateEdit->setDate(lastUsedPostDate.date());
1345         } else {
1346             d->ui->dateEdit->setDate(QDate::currentDate());
1347         }
1348 
1349         d->ui->creditDebitEdit->setSharesCommodity(commodity);
1350         // the default exchange rate is 1 so we don't need to set it here
1351 
1352     } else {
1353         d->loadTransaction(idx);
1354     }
1355 
1356     // set focus to first tab field once we return to event loop
1357     const auto tabOrder = property("kmm_currenttaborder").toStringList();
1358     if (!tabOrder.isEmpty()) {
1359         for (const auto& widgetName : tabOrder) {
1360             const auto focusWidget = findChild<QWidget*>(widgetName);
1361             if (focusWidget && focusWidget->isVisibleTo(this)) {
1362                 QMetaObject::invokeMethod(focusWidget, "setFocus", Qt::QueuedConnection);
1363                 break;
1364             }
1365         }
1366     }
1367 }
1368 
1369 
1370 void NewTransactionEditor::editSplits()
1371 {
1372     d->editSplits() == QDialog::Accepted ? acceptEdit() : reject();
1373 }
1374 
1375 MyMoneyMoney NewTransactionEditor::transactionAmount() const
1376 {
1377     auto amount = d->ui->creditDebitEdit->value();
1378     if (amount.isZero()) {
1379         amount = -d->splitsSum();
1380     }
1381     return amount;
1382 }
1383 
1384 MyMoneyTransaction NewTransactionEditor::transaction() const
1385 {
1386     MyMoneyTransaction t;
1387 
1388     if (!d->m_transaction.id().isEmpty()) {
1389         t = d->m_transaction;
1390     } else {
1391         // use the commodity of the account
1392         t.setCommodity(d->m_transaction.commodity());
1393 
1394         // we keep the date when adding a new transaction
1395         // for the next new one
1396         KMyMoneySettings::setLastUsedPostDate(d->ui->dateEdit->date().startOfDay());
1397     }
1398 
1399     // first remove the splits that are gone
1400     for (const auto& split : t.splits()) {
1401         if (split.id() == d->m_split.id()) {
1402             continue;
1403         }
1404         const auto rows = d->splitModel.rowCount();
1405         int row;
1406         for (row = 0; row < rows; ++row) {
1407             const QModelIndex index = d->splitModel.index(row, 0);
1408             if (index.data(eMyMoney::Model::IdRole).toString() == split.id()) {
1409                 break;
1410             }
1411         }
1412 
1413         // if the split is not in the model, we get rid of it
1414         if (d->splitModel.rowCount() == row) {
1415             t.removeSplit(split);
1416         }
1417     }
1418 
1419     // now we update the split we are opened for
1420     MyMoneySplit sp(d->m_split);
1421 
1422     // in case the transaction does not have a split
1423     // at this point, we need to make sure that we
1424     // add the first one and don't try to modify it
1425     // we do so by clearing its id
1426     if (t.splitCount() == 0) {
1427         sp.clearId();
1428     }
1429 
1430     sp.setNumber(d->ui->numberEdit->text());
1431     sp.setMemo(d->ui->memoEdit->toPlainText());
1432     // setting up the shares and value members. In case there is
1433     // no or more than two splits, we can take the amount shown
1434     // in the widgets directly. In case of 2 splits, we take
1435     // the negative value of the second split (the one in the
1436     // splitModel) and use it as value and shares since the
1437     // displayed value in the widget may be shown in a different
1438     // currency
1439     if (d->splitModel.rowCount() == 1) {
1440         const QModelIndex idx = d->splitModel.index(0, 0);
1441         const auto val = idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>();
1442         sp.setShares(-val);
1443         sp.setValue(-val);
1444         sp.setPrice(MyMoneyMoney::ONE);
1445     } else {
1446         sp.setShares(d->ui->creditDebitEdit->value());
1447         sp.setValue(d->ui->creditDebitEdit->value());
1448     }
1449 
1450     if (sp.reconcileFlag() != eMyMoney::Split::State::Reconciled && !sp.reconcileDate().isValid()
1451         && d->ui->statusCombo->currentIndex() == (int)eMyMoney::Split::State::Reconciled) {
1452         sp.setReconcileDate(QDate::currentDate());
1453     }
1454 
1455     sp.setReconcileFlag(static_cast<eMyMoney::Split::State>(d->ui->statusCombo->currentIndex()));
1456 
1457     const auto payeeRow = d->ui->payeeEdit->currentIndex();
1458     const auto payeeIdx = d->payeesModel->index(payeeRow, 0);
1459     sp.setPayeeId(payeeIdx.data(eMyMoney::Model::IdRole).toString());
1460 
1461     if (sp.id().isEmpty()) {
1462         t.addSplit(sp);
1463     } else {
1464         t.modifySplit(sp);
1465     }
1466     t.setPostDate(d->ui->dateEdit->date());
1467 
1468     // now update and add what we have in the model
1469     d->splitModel.addSplitsToTransaction(t);
1470 
1471     return t;
1472 }
1473 
1474 QStringList NewTransactionEditor::saveTransaction(const QStringList& selectedJournalEntries)
1475 {
1476     auto t = transaction();
1477 
1478     auto selection(selectedJournalEntries);
1479     connect(MyMoneyFile::instance()->journalModel(), &JournalModel::idChanged, this, [&](const QString& currentId, const QString& previousId) {
1480         selection.replaceInStrings(previousId, currentId);
1481     });
1482 
1483     MyMoneyFileTransaction ft;
1484     try {
1485         if (t.id().isEmpty()) {
1486             MyMoneyFile::instance()->addTransaction(t);
1487         } else {
1488             t.setImported(false);
1489             MyMoneyFile::instance()->modifyTransaction(t);
1490         }
1491         ft.commit();
1492 
1493     } catch (const MyMoneyException& e) {
1494         qDebug() << Q_FUNC_INFO << "something went wrong" << e.what();
1495         selection = selectedJournalEntries;
1496     }
1497 
1498     return selection;
1499 }
1500 
1501 bool NewTransactionEditor::eventFilter(QObject* o, QEvent* e)
1502 {
1503     auto cb = qobject_cast<QComboBox*>(o);
1504     if (o) {
1505         // filter out wheel events for combo boxes if the popup view is not visible
1506         if ((e->type() == QEvent::Wheel) && !cb->view()->isVisible()) {
1507             return true;
1508         }
1509 
1510         if (e->type() == QEvent::FocusOut) {
1511             if (cb == d->ui->categoryCombo) {
1512                 if (needCreateCategory(d->ui->categoryCombo)) {
1513                     createCategory(d->ui->categoryCombo, defaultCategoryType(d->ui->creditDebitEdit));
1514                 }
1515 
1516             } else if (o == d->ui->payeeEdit) {
1517                 if (needCreatePayee(cb)) {
1518                     createPayee(cb);
1519 
1520                 } else if (!cb->currentText().isEmpty()) {
1521                     const auto index(cb->findText(cb->currentText()));
1522                     cb->setCurrentIndex(index);
1523                     // check if category is filled and fill with
1524                     // default for payee if one is setup
1525                     d->defaultCategoryAssignment();
1526                 }
1527             } else if (o == d->ui->tagContainer->tagCombo()) {
1528                 if (needCreateTag(cb)) {
1529                     createTag(d->ui->tagContainer);
1530                 }
1531             }
1532         } else if (e->type() == QEvent::FocusIn) {
1533             if (o == d->ui->payeeEdit) {
1534                 // set case sensitivity so that a payee with the same spelling
1535                 // but different case will be presented in the popup view of
1536                 // the completion box. We need to do that because the CaseSensitive
1537                 // mode is set when the focus leaves the widget (see above).
1538                 d->ui->payeeEdit->completer()->setCaseSensitivity(Qt::CaseInsensitive);
1539             }
1540         }
1541     }
1542     return QWidget::eventFilter(o, e);
1543 }
1544 
1545 QDate NewTransactionEditor::postDate() const
1546 {
1547     return d->ui->dateEdit->date();
1548 }
1549 
1550 void NewTransactionEditor::setShowAccountCombo(bool show) const
1551 {
1552     d->ui->accountLabel->setVisible(show);
1553     d->ui->accountCombo->setVisible(show);
1554     d->ui->topMarginWidget->setVisible(show);
1555     d->ui->accountCombo->setSplitActionVisible(false);
1556 }
1557 
1558 void NewTransactionEditor::setShowButtons(bool show) const
1559 {
1560     d->ui->enterButton->setVisible(show);
1561     d->ui->cancelButton->setVisible(show);
1562 }
1563 
1564 void NewTransactionEditor::setShowNumberWidget(bool show) const
1565 {
1566     d->ui->numberLabel->setVisible(show);
1567     d->ui->numberEdit->setVisible(show);
1568 }
1569 
1570 void NewTransactionEditor::setAccountId(const QString& accountId)
1571 {
1572     d->ui->accountCombo->setSelected(accountId);
1573 }
1574 
1575 void NewTransactionEditor::setReadOnly(bool readOnly)
1576 {
1577     if (isReadOnly() != readOnly) {
1578         TransactionEditorBase::setReadOnly(readOnly);
1579         if (readOnly) {
1580             d->frameCollection->removeWidget(d->ui->enterButton);
1581             d->ui->enterButton->setDisabled(true);
1582         } else {
1583             // no need to enable the enter button here as the
1584             // frameCollection will take care of it anyway
1585             d->frameCollection->addWidget(d->ui->enterButton);
1586         }
1587     }
1588 }
1589 
1590 void NewTransactionEditor::setupUi(QWidget* parent)
1591 {
1592     if (d->tabOrderUi == nullptr) {
1593         d->tabOrderUi = new Ui::NewTransactionEditor;
1594     }
1595     d->tabOrderUi->setupUi(parent);
1596     d->tabOrderUi->accountLabel->setVisible(false);
1597     d->tabOrderUi->accountCombo->setVisible(false);
1598 }
1599 
1600 void NewTransactionEditor::storeTabOrder(const QStringList& tabOrder)
1601 {
1602     TransactionEditorBase::storeTabOrder(QLatin1String("stdTransactionEditor"), tabOrder);
1603 }
1604 
1605 void NewTransactionEditor::slotSettingsChanged()
1606 {
1607     d->categoriesModel->setShowAllEntries(KMyMoneySettings::showAllAccounts());
1608     d->accountsModel->setShowAllEntries(KMyMoneySettings::showAllAccounts());
1609 }
1610 
1611 WidgetHintFrameCollection* NewTransactionEditor::widgetHintFrameCollection() const
1612 {
1613     return d->frameCollection;
1614 }
1615 
1616 void NewTransactionEditor::setKeepCategoryAmount(bool keepCategoryAmount)
1617 {
1618     d->keepCategoryAmount = keepCategoryAmount;
1619 }
1620 
1621 bool NewTransactionEditor::isTransactionDataValid() const
1622 {
1623     return d->checkForValidTransaction(false);
1624 }