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

0001 /*
0002     SPDX-FileCopyrightText: 2019-2021 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "investtransactioneditor.h"
0007 
0008 // ----------------------------------------------------------------------------
0009 // QT Includes
0010 
0011 #include <QAbstractItemView>
0012 #include <QCompleter>
0013 #include <QDebug>
0014 #include <QGlobalStatic>
0015 #include <QSortFilterProxyModel>
0016 #include <QStringList>
0017 #include <QStringListModel>
0018 
0019 // ----------------------------------------------------------------------------
0020 // KDE Includes
0021 
0022 #include <KLocalizedString>
0023 #include <KDescendantsProxyModel>
0024 
0025 // ----------------------------------------------------------------------------
0026 // Project Includes
0027 
0028 #include "dialogenums.h"
0029 #include "icons.h"
0030 #include "investactivities.h"
0031 #include "journalmodel.h"
0032 #include "kcurrencycalculator.h"
0033 #include "kmymoneysettings.h"
0034 #include "kmymoneyutils.h"
0035 #include "mymoneyaccount.h"
0036 #include "mymoneyexception.h"
0037 #include "mymoneyfile.h"
0038 #include "mymoneyprice.h"
0039 #include "mymoneysecurity.h"
0040 #include "mymoneysplit.h"
0041 #include "mymoneytransaction.h"
0042 #include "securitiesmodel.h"
0043 #include "splitdialog.h"
0044 #include "splitmodel.h"
0045 #include "statusmodel.h"
0046 #include "ui_investtransactioneditor.h"
0047 #include "widgethintframe.h"
0048 
0049 using namespace Icons;
0050 
0051 class InvestTransactionEditor::Private
0052 {
0053     Q_DISABLE_COPY_MOVE(Private)
0054 
0055 public:
0056     Private(InvestTransactionEditor* parent)
0057         : q(parent)
0058         , ui(new Ui_InvestTransactionEditor)
0059         , tabOrderUi(nullptr)
0060         , accountsModel(new AccountNamesFilterProxyModel(parent))
0061         , feesModel(new AccountNamesFilterProxyModel(parent))
0062         , interestModel(new AccountNamesFilterProxyModel(parent))
0063         , activitiesModel(new QStringListModel(parent))
0064         , securitiesModel(new QSortFilterProxyModel(parent))
0065         , securityFilterModel(new AccountsProxyModel(parent))
0066         , accountsListModel(new KDescendantsProxyModel(parent))
0067         , feeSplitHelper(nullptr)
0068         , interestSplitHelper(nullptr)
0069         , currentActivity(nullptr)
0070         , feeSplitModel(new SplitModel(parent, &undoStack))
0071         , interestSplitModel(new SplitModel(parent, &undoStack))
0072         , loadedFromModel(false)
0073         , bypassUserPriceUpdate(false)
0074     {
0075         accountsModel->setObjectName("InvestTransactionEditor::accountsModel");
0076         feeSplitModel->setObjectName("FeesSplitModel");
0077         interestSplitModel->setObjectName("InterestSplitModel");
0078 
0079         // keep in sync with eMyMoney::Split::InvestmentTransactionType
0080         QStringList activityItems{
0081             i18nc("@item:inlistbox transaction type", "Buy shares"),
0082             i18nc("@item:inlistbox transaction type", "Sell shares"),
0083             i18nc("@item:inlistbox transaction type", "Dividend"),
0084             i18nc("@item:inlistbox transaction type", "Reinvest dividend"),
0085             i18nc("@item:inlistbox transaction type", "Yield"),
0086             i18nc("@item:inlistbox transaction type", "Add shares"),
0087             i18nc("@item:inlistbox transaction type", "Remove shares"),
0088             i18nc("@item:inlistbox transaction type", "Split shares"),
0089             i18nc("@item:inlistbox transaction type", "Interest Income"),
0090         };
0091 
0092         activitiesModel->setStringList(activityItems);
0093     }
0094 
0095     ~Private()
0096     {
0097         delete ui;
0098     }
0099 
0100     void dumpSplitModel(const QString& header, const QAbstractItemModel* model)
0101     {
0102         const auto rows = model->rowCount();
0103         qDebug() << header;
0104         for (int row = 0; row < rows; ++row) {
0105             const auto idx = model->index(row, 0);
0106             qDebug() << row << idx.data(eMyMoney::Model::IdRole).toString() << idx.data(eMyMoney::Model::SplitAccountIdRole).toString()
0107                      << idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>().formatMoney(100) << idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>().formatMoney(100);
0108         }
0109     }
0110     void createStatusEntry(eMyMoney::Split::State status);
0111     bool isDatePostOpeningDate(const QDate& date, const QString& accountId);
0112 
0113     bool postdateChanged(const QDate& date);
0114     bool categoryChanged(SplitModel* model, const QString& accountId, AmountEdit* widget, const MyMoneyMoney& factor);
0115     bool checkForValidTransaction(bool doUserInteraction = true);
0116 
0117     void setSecurity(const MyMoneySecurity& sec);
0118 
0119     bool amountChanged(SplitModel* model, AmountEdit* widget, const MyMoneyMoney& transactionFactor);
0120     bool isDividendOrYield(eMyMoney::Split::InvestmentTransactionType type) const;
0121 
0122     void scheduleUpdateTotalAmount();
0123     void updateWidgetState();
0124     void protectWidgetsForClosedAccount();
0125     void setupTabOrder();
0126 
0127     void editSplits(SplitModel* sourceSplitModel, AmountEdit* amountEdit, const MyMoneyMoney& transactionFactor);
0128     void removeUnusedSplits(MyMoneyTransaction& t, SplitModel* splitModel);
0129     void addSplits(MyMoneyTransaction& t, SplitModel* splitModel);
0130     void setupParentInvestmentAccount(const QString& accountId);
0131     QModelIndex adjustToSecuritySplitIdx(const QModelIndex& idx);
0132 
0133     void loadFeeAndInterestAmountEdits();
0134     void adjustSharesCommodity(AmountEdit* amountEdit, const QString& accountId);
0135     void setupAssetAccount(const QString& accountId);
0136 
0137     InvestTransactionEditor* q;
0138     Ui_InvestTransactionEditor* ui;
0139     Ui_InvestTransactionEditor* tabOrderUi;
0140 
0141     // models for UI elements
0142     AccountNamesFilterProxyModel* accountsModel;
0143     AccountNamesFilterProxyModel* feesModel;
0144     AccountNamesFilterProxyModel* interestModel;
0145     QStringListModel* activitiesModel;
0146     QSortFilterProxyModel* securitiesModel;
0147     AccountsProxyModel* securityFilterModel;
0148     KDescendantsProxyModel* accountsListModel;
0149     KMyMoneyAccountComboSplitHelper* feeSplitHelper;
0150     KMyMoneyAccountComboSplitHelper* interestSplitHelper;
0151 
0152     QUndoStack undoStack;
0153     Invest::Activity* currentActivity;
0154 
0155     // the selected security and the account holding it
0156     MyMoneySecurity security;
0157     // and its trading currency
0158     MyMoneySecurity transactionCurrency;
0159 
0160     // the asset or brokerage account
0161     MyMoneyAccount assetAccount;
0162     // and its currency
0163     MyMoneySecurity assetSecurity;
0164 
0165     // the containing investment account (parent of stockAccount)
0166     MyMoneyAccount parentAccount;
0167 
0168     // the transaction
0169     MyMoneyTransaction transaction;
0170 
0171     // the various splits
0172     MyMoneySplit stockSplit;
0173     MyMoneySplit assetSplit;
0174     SplitModel* feeSplitModel;
0175     SplitModel* interestSplitModel;
0176 
0177     // exchange rate information for assetSplit
0178     MyMoneyPrice assetPrice;
0179 
0180     bool loadedFromModel;
0181 
0182     /**
0183      * Flag to bypass the user dialog to modify exchange rate information.
0184      * This is used during the loading of a transaction, when data is
0185      * changed due to the load operation but no user interaction is
0186      * wanted.
0187      */
0188     bool bypassUserPriceUpdate;
0189 };
0190 
0191 void InvestTransactionEditor::Private::removeUnusedSplits(MyMoneyTransaction& t, SplitModel* splitModel)
0192 {
0193     for (const auto& sp : qAsConst(t.splits())) {
0194         if (sp.id() == stockSplit.id()) {
0195             continue;
0196         }
0197         const auto rows = splitModel->rowCount();
0198         int row;
0199         for (row = 0; row < rows; ++row) {
0200             const QModelIndex index = splitModel->index(row, 0);
0201             if (index.data(eMyMoney::Model::IdRole).toString() == sp.id()) {
0202                 break;
0203             }
0204         }
0205 
0206         // if the split is not in the model, we get rid of it
0207         if (splitModel->rowCount() == row) {
0208             t.removeSplit(sp);
0209         }
0210     }
0211 }
0212 
0213 void InvestTransactionEditor::Private::addSplits(MyMoneyTransaction& t, SplitModel* splitModel)
0214 {
0215     for (int row = 0; row < splitModel->rowCount(); ++row) {
0216         const auto idx = splitModel->index(row, 0);
0217         MyMoneySplit s;
0218         const auto splitId = idx.data(eMyMoney::Model::IdRole).toString();
0219         // Extract the split from the transaction if
0220         // it already exists. Otherwise it remains
0221         // an empty split and will be added later.
0222         try {
0223             s = t.splitById(splitId);
0224         } catch(const MyMoneyException&) {
0225         }
0226         s.setMemo(idx.data(eMyMoney::Model::SplitMemoRole).toString());
0227         s.setAccountId(idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0228         s.setShares(idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0229         s.setValue(idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>());
0230 
0231         if (s.id().isEmpty() || splitModel->isNewSplitId(s.id())) {
0232             s.clearId();
0233             t.addSplit(s);
0234         } else {
0235             t.modifySplit(s);
0236         }
0237     }
0238 }
0239 
0240 bool InvestTransactionEditor::Private::isDatePostOpeningDate(const QDate& date, const QString& accountId)
0241 {
0242     bool rc = true;
0243 
0244     try {
0245         MyMoneyAccount account = MyMoneyFile::instance()->account(accountId);
0246 
0247         // we don't check for categories
0248         if (!account.isIncomeExpense()) {
0249             if (date < account.openingDate())
0250                 rc = false;
0251         }
0252     } catch (MyMoneyException&) {
0253         qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO;
0254     }
0255     return rc;
0256 }
0257 
0258 bool InvestTransactionEditor::Private::postdateChanged(const QDate& date)
0259 {
0260     bool rc = true;
0261     WidgetHintFrame::hide(ui->dateEdit, i18n("The posting date of the transaction."));
0262 
0263     QStringList accountIds;
0264 
0265     auto collectAccounts = [&](const SplitModel* model) {
0266         const auto rows = model->rowCount();
0267         for (int row = 0; row < rows; ++row) {
0268             const auto index = model->index(row, 0);
0269             accountIds << index.data(eMyMoney::Model::SplitAccountIdRole).toString();
0270         }
0271     };
0272 
0273     // collect all account ids
0274     accountIds << parentAccount.id();
0275     if (currentActivity->feesRequired() != Invest::Activity::Unused) {
0276         collectAccounts(feeSplitModel);
0277     }
0278     if (currentActivity->interestRequired() != Invest::Activity::Unused) {
0279         collectAccounts(interestSplitModel);
0280     }
0281 
0282     for (const auto& accountId : qAsConst(accountIds)) {
0283         if (!isDatePostOpeningDate(date, accountId)) {
0284             const auto account = MyMoneyFile::instance()->account(accountId);
0285             WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is prior to the opening date of account <b>%1</b>.", account.name()));
0286             rc = false;
0287             break;
0288         }
0289     }
0290     return rc;
0291 }
0292 
0293 bool InvestTransactionEditor::Private::categoryChanged(SplitModel* model, const QString& accountId, AmountEdit* amountEdit, const MyMoneyMoney& factor)
0294 {
0295     bool rc = true;
0296     if (accountId.isEmpty()) {
0297         // in case the user cleared the category, we need
0298         // to make sure that there are no leftovers in the
0299         // split model so we clear it. This can only happen
0300         // for single split categories, so at most we
0301         // have to clear one item
0302         if (model->rowCount() == 1) {
0303             const auto idx = model->index(0, 0);
0304             const auto s = model->itemByIndex(idx);
0305             model->removeItem(s);
0306         }
0307         return true;
0308     }
0309 
0310     if (model->rowCount() <= 1) {
0311         try {
0312             MyMoneyAccount category = MyMoneyFile::instance()->account(accountId);
0313 
0314             // make sure we have a split in the model
0315             if (model->rowCount() == 0) {
0316                 // add an empty split
0317                 MyMoneySplit s;
0318                 model->addItem(s);
0319             }
0320 
0321             const auto index = model->index(0, 0);
0322             model->setData(index, accountId, eMyMoney::Model::SplitAccountIdRole);
0323 
0324             // extract the categories currency
0325             const auto accountIdx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
0326             const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0327             const auto currency = MyMoneyFile::instance()->currenciesModel()->itemById(currencyId);
0328 
0329             // in case the commodity changes, we need to update the shares part
0330             if (currency.id() != amountEdit->sharesCommodity().id()) {
0331                 // switch to value display so that we show the transaction commodity
0332                 // for single currency data entry this does not have an effect
0333                 amountEdit->setDisplayState(MultiCurrencyEdit::DisplayValue);
0334                 amountEdit->setSharesCommodity(currency);
0335                 auto sharesAmount = amountEdit->value();
0336                 if (!sharesAmount.isZero()) {
0337                     amountEdit->setShares(sharesAmount);
0338                     KCurrencyCalculator::updateConversion(amountEdit, ui->dateEdit->date());
0339                 }
0340             }
0341 
0342             model->setData(index, QVariant::fromValue<MyMoneyMoney>(factor * amountEdit->value()), eMyMoney::Model::SplitValueRole);
0343             model->setData(index, QVariant::fromValue<MyMoneyMoney>(factor * amountEdit->shares()), eMyMoney::Model::SplitSharesRole);
0344 
0345         } catch (MyMoneyException&) {
0346             qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO;
0347         }
0348     }
0349 
0350     return rc;
0351 }
0352 
0353 bool InvestTransactionEditor::Private::checkForValidTransaction(bool doUserInteraction)
0354 {
0355     QStringList infos;
0356     bool rc = true;
0357     if (!postdateChanged(ui->dateEdit->date())) {
0358         infos << ui->dateEdit->toolTip();
0359         rc = false;
0360     }
0361 
0362     if (q->needCreateCategory(ui->feesCombo) || q->needCreateCategory(ui->interestCombo) || q->needCreateCategory(ui->assetAccountCombo)) {
0363         rc = false;
0364     }
0365 
0366     if (doUserInteraction) {
0367         /// @todo add dialog here that shows the @a infos about the problem
0368     }
0369     return rc;
0370 }
0371 
0372 void InvestTransactionEditor::Private::setSecurity(const MyMoneySecurity& sec)
0373 {
0374     if (sec.tradingCurrency() != security.tradingCurrency()) {
0375         transactionCurrency = MyMoneyFile::instance()->currency(sec.tradingCurrency());
0376         ui->totalAmountEdit->setValueCommodity(transactionCurrency);
0377         transaction.setCommodity(sec.tradingCurrency());
0378         feeSplitModel->setTransactionCommodity(sec.tradingCurrency());
0379         interestSplitModel->setTransactionCommodity(sec.tradingCurrency());
0380         loadFeeAndInterestAmountEdits();
0381 
0382         auto haveValue = [&](const SplitModel* model) {
0383             const auto rows = model->rowCount();
0384             for (int row = 0; row < rows; ++row) {
0385                 const auto idx = model->index(row, 0);
0386                 if (!idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>().isZero()) {
0387                     return true;
0388                 }
0389             }
0390             return false;
0391         };
0392 
0393         bool needWarning = !assetSplit.value().isZero();
0394         if (currentActivity) {
0395             needWarning |= ((currentActivity->feesRequired() != Invest::Activity::Unused) && haveValue(feeSplitModel));
0396             needWarning |= ((currentActivity->interestRequired() != Invest::Activity::Unused) && haveValue(interestSplitModel));
0397         }
0398 
0399         if (needWarning) {
0400             ui->infoMessage->setText(i18nc("@info:usagetip", "The transaction commodity has been changed which will possibly make all price information invalid. Please check them."));
0401             if (!ui->infoMessage->isShowAnimationRunning()) {
0402                 ui->infoMessage->animatedShow();
0403                 Q_EMIT q->editorLayoutChanged();
0404             }
0405         }
0406     }
0407 
0408     security = sec;
0409 
0410     // update the precision to that used by the new security
0411     ui->sharesAmountEdit->setPrecision(MyMoneyMoney::denomToPrec(security.smallestAccountFraction()));
0412 }
0413 
0414 bool InvestTransactionEditor::Private::amountChanged(SplitModel* model, AmountEdit* amountEdit, const MyMoneyMoney& transactionFactor)
0415 {
0416     bool rc = true;
0417     if (!amountEdit->text().isEmpty() && (model->rowCount() <= 1)) {
0418         try {
0419             MyMoneyMoney shares;
0420             if (model->rowCount() == 1) {
0421                 const auto index = model->index(0, 0);
0422 
0423                 // check if there is a change in the values other than simply reverting the sign
0424                 // and get an updated price in that case
0425                 if ((index.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>() != -amountEdit->shares())
0426                     || (index.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>() != -amountEdit->value())) {
0427                     KCurrencyCalculator::updateConversion(amountEdit, ui->dateEdit->date());
0428                 }
0429 
0430                 model->setData(index, QVariant::fromValue<MyMoneyMoney>((amountEdit->value() * transactionFactor)), eMyMoney::Model::SplitValueRole);
0431                 model->setData(index, QVariant::fromValue<MyMoneyMoney>((amountEdit->shares() * transactionFactor)), eMyMoney::Model::SplitSharesRole);
0432             }
0433 
0434         } catch (MyMoneyException&) {
0435             rc = false;
0436             qDebug() << "Ooops: something went wrong in" << Q_FUNC_INFO;
0437         }
0438     } else {
0439         /// @todo ask what to do: if the rest of the splits is the same amount we could simply reverse the sign
0440         /// of all splits, otherwise we could ask if the user wants to start the split editor or anything else.
0441     }
0442     return rc;
0443 }
0444 
0445 bool InvestTransactionEditor::Private::isDividendOrYield(eMyMoney::Split::InvestmentTransactionType type) const
0446 {
0447     return (type == eMyMoney::Split::InvestmentTransactionType::Dividend) || (type == eMyMoney::Split::InvestmentTransactionType::Yield);
0448 }
0449 
0450 void InvestTransactionEditor::Private::editSplits(SplitModel* sourceSplitModel, AmountEdit* amountEdit, const MyMoneyMoney& transactionFactor)
0451 {
0452     SplitModel splitModel(q, nullptr, *sourceSplitModel);
0453 
0454     // create an empty split at the end
0455     // used to create new splits
0456     splitModel.appendEmptySplit();
0457 
0458     QPointer<SplitDialog> splitDialog = new SplitDialog(transactionCurrency, MyMoneyMoney::autoCalc, parentAccount.fraction(), transactionFactor, q);
0459     splitDialog->setModel(&splitModel);
0460 
0461     int rc = splitDialog->exec();
0462 
0463     if (splitDialog && (rc == QDialog::Accepted)) {
0464         // remove that empty split again before we update the splits
0465         splitModel.removeEmptySplit();
0466 
0467         // copy the splits model contents
0468         *sourceSplitModel = splitModel;
0469 
0470         // update the transaction amount
0471         amountEdit->setSharesCommodity(amountEdit->valueCommodity());
0472         auto amountShares = splitDialog->transactionAmount() * transactionFactor;
0473         amountEdit->setValue(amountShares);
0474 
0475         // the price might have been changed, so we have to update our copy
0476         // but only if there is one counter split
0477         if (sourceSplitModel->rowCount() == 1) {
0478             const auto idx = sourceSplitModel->index(0, 0);
0479             amountShares = idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>();
0480 
0481             adjustSharesCommodity(amountEdit, idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0482 
0483             // make sure to show the value in the widget
0484             // according to the currency presented
0485         }
0486         amountEdit->setShares(amountShares * transactionFactor);
0487 
0488         updateWidgetState();
0489     }
0490 
0491     if (splitDialog) {
0492         splitDialog->deleteLater();
0493     }
0494 }
0495 
0496 void InvestTransactionEditor::Private::setupParentInvestmentAccount(const QString& accountId)
0497 {
0498     auto const file = MyMoneyFile::instance();
0499     auto const model = file->accountsModel();
0500 
0501     // extract account information from model
0502     const auto index = model->indexById(accountId);
0503     parentAccount = model->itemByIndex(index);
0504 
0505     // show child accounts in the combo box
0506     securitiesModel->setFilterFixedString(accountId);
0507 }
0508 
0509 QModelIndex InvestTransactionEditor::Private::adjustToSecuritySplitIdx(const QModelIndex& index)
0510 {
0511     if (!index.isValid()) {
0512         return {};
0513     }
0514     const auto first = MyMoneyFile::instance()->journalModel()->adjustToFirstSplitIdx(index);
0515     const auto id = first.data(eMyMoney::Model::IdRole).toString();
0516 
0517     const auto rows = first.data(eMyMoney::Model::TransactionSplitCountRole).toInt();
0518     const auto endRow = first.row() + rows;
0519     for(int row = first.row(); row < endRow; ++row) {
0520         const auto idx = index.model()->index(row, 0);
0521         const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0522         const auto account = MyMoneyFile::instance()->accountsModel()->itemById(accountId);
0523         if (account.isInvest()) {
0524             return idx;
0525         }
0526     }
0527     return {};
0528 }
0529 
0530 void InvestTransactionEditor::Private::protectWidgetsForClosedAccount()
0531 {
0532     const auto securityAccont = MyMoneyFile::instance()->accountsModel()->itemById(stockSplit.accountId());
0533     const bool closed = securityAccont.isClosed();
0534     ui->sharesAmountEdit->setReadOnly(closed);
0535     ui->securityAccountCombo->setDisabled(closed);
0536     ui->activityCombo->setDisabled(closed);
0537 }
0538 
0539 void InvestTransactionEditor::Private::updateWidgetState()
0540 {
0541     WidgetHintFrame::hide(ui->feesCombo, i18nc("@info:tooltip", "Category for fees"));
0542     WidgetHintFrame::hide(ui->feesAmountEdit, i18nc("@info:tooltip", "Amount of fees"));
0543     WidgetHintFrame::hide(ui->interestCombo, i18nc("@info:tooltip", "Category for interest"));
0544     WidgetHintFrame::hide(ui->interestAmountEdit, i18nc("@info:tooltip", "Amount of interest"));
0545     WidgetHintFrame::hide(ui->assetAccountCombo, i18nc("@info:tooltip", "Asset or brokerage account"));
0546     WidgetHintFrame::hide(ui->priceAmountEdit, i18nc("@info:tooltip", "Price information for this transaction"));
0547 
0548     if (ui->securityAccountCombo->isEnabled()) {
0549         WidgetHintFrame::hide(ui->securityAccountCombo, i18nc("@info:tooltip", "Security for this transaction"));
0550         ui->activityCombo->setToolTip(i18nc("@info:tooltip", "Select the activity for this transaction."));
0551     } else {
0552         WidgetHintFrame::hide(ui->securityAccountCombo,
0553                               i18nc("@info:tooltip", "The security for this transaction cannot be modified because the security account is closed."));
0554         ui->activityCombo->setToolTip(i18nc("@info:tooltip", "This activity cannot be modified because the security account is closed."));
0555     }
0556 
0557     // all the other logic needs a valid activity
0558     if (currentActivity == nullptr) {
0559         return;
0560     }
0561 
0562     const auto widget = ui->sharesAmountEdit;
0563     switch(currentActivity->type()) {
0564     default:
0565         if (ui->securityAccountCombo->isEnabled()) {
0566             WidgetHintFrame::hide(widget, i18nc("@info:tooltip", "Number of shares"));
0567         } else {
0568             WidgetHintFrame::hide(widget, i18nc("@info:tooltip", "The number of shares cannot be modified because the security account is closed."));
0569         }
0570         if (widget->isVisible()) {
0571             if (widget->value().isZero()) {
0572                 WidgetHintFrame::show(widget, i18nc("@info:tooltip", "Enter number of shares for this transaction"));
0573             }
0574         }
0575         break;
0576     case eMyMoney::Split::InvestmentTransactionType::SplitShares:
0577         if (ui->securityAccountCombo->isEnabled()) {
0578             WidgetHintFrame::hide(widget, i18nc("@info:tooltip", "Split ratio"));
0579         } else {
0580             WidgetHintFrame::hide(widget, i18nc("@info:tooltip", "The split ratio cannot be modified because the security account is closed."));
0581         }
0582         if (widget->isVisible()) {
0583             if (widget->value().isZero()) {
0584                 WidgetHintFrame::show(widget, i18nc("@info:tooltip", "Enter the split ratio for this transaction"));
0585             }
0586         }
0587         break;
0588     }
0589 
0590     switch(currentActivity->priceRequired()) {
0591     case Invest::Activity::Unused:
0592         break;
0593     case Invest::Activity::Optional:
0594     case Invest::Activity::Mandatory:
0595         if (ui->priceAmountEdit->value().isZero()) {
0596             WidgetHintFrame::show(ui->priceAmountEdit, i18nc("@info:tooltip", "Enter price information for this transaction"));
0597         }
0598         break;
0599     }
0600 
0601     QString accountId;
0602     switch(currentActivity->assetAccountRequired()) {
0603     case Invest::Activity::Unused:
0604         break;
0605     case Invest::Activity::Optional:
0606     case Invest::Activity::Mandatory:
0607         accountId = ui->assetAccountCombo->getSelected();
0608         if (MyMoneyFile::instance()->isStandardAccount(accountId)) {
0609             accountId.clear();
0610         }
0611         if (accountId.isEmpty()) {
0612             WidgetHintFrame::show(ui->assetAccountCombo, i18nc("@info:tooltip", "Select account to balance the transaction"));
0613         }
0614         break;
0615     }
0616 
0617     if (!currentActivity->haveFees(currentActivity->feesRequired())) {
0618         if (ui->feesCombo->currentText().isEmpty()) {
0619             WidgetHintFrame::show(ui->feesCombo, i18nc("@info:tooltip", "Enter category for fees"));
0620         }
0621         if (ui->feesAmountEdit->value().isZero()) {
0622             WidgetHintFrame::show(ui->feesAmountEdit, i18nc("@info:tooltip", "Enter amount of fees"));
0623         }
0624     }
0625 
0626     if (!currentActivity->haveInterest(currentActivity->interestRequired())) {
0627         if (ui->interestCombo->currentText().isEmpty()) {
0628             WidgetHintFrame::show(ui->interestCombo, i18nc("@info:tooltip", "Enter category for interest"));
0629         }
0630         if (ui->interestAmountEdit->value().isZero()) {
0631             WidgetHintFrame::show(ui->interestAmountEdit, i18nc("@info:tooltip", "Enter amount of interest"));
0632         }
0633     }
0634 
0635     if (ui->securityAccountCombo->currentIndex() == -1) {
0636         WidgetHintFrame::show(ui->securityAccountCombo, i18nc("@info:tooltip", "Select the security for this transaction"));
0637     }
0638 }
0639 
0640 void InvestTransactionEditor::Private::loadFeeAndInterestAmountEdits()
0641 {
0642     auto loadAmountEdit = [&](SplitModel* model, AmountEdit* amountEdit) {
0643         amountEdit->setReadOnly(false);
0644         amountEdit->setCommodity(transactionCurrency);
0645         switch (model->rowCount()) {
0646         case 0:
0647             amountEdit->clear();
0648             break;
0649         case 1: {
0650             const auto idx = model->index(0, 0);
0651             amountEdit->setValue(idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>().abs());
0652             amountEdit->setShares(idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>().abs());
0653             adjustSharesCommodity(amountEdit, idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0654         } break;
0655         default:
0656             amountEdit->setValue(model->valueSum().abs());
0657             amountEdit->setShares(model->valueSum().abs());
0658             amountEdit->setReadOnly(true);
0659             break;
0660         }
0661     };
0662 
0663     // possibly update the currency on the fees and interest
0664     loadAmountEdit(feeSplitModel, ui->feesAmountEdit);
0665     loadAmountEdit(interestSplitModel, ui->interestAmountEdit);
0666 }
0667 
0668 void InvestTransactionEditor::Private::adjustSharesCommodity(AmountEdit* amountEdit, const QString& accountId)
0669 {
0670     // adjust the commodity for the shares
0671     const auto accountIdx = MyMoneyFile::instance()->accountsModel()->indexById(accountId);
0672     const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0673     const auto currency = MyMoneyFile::instance()->currenciesModel()->itemById(currencyId);
0674     amountEdit->setSharesCommodity(currency);
0675 }
0676 
0677 void InvestTransactionEditor::Private::setupAssetAccount(const QString& accountId)
0678 {
0679     const auto file = MyMoneyFile::instance();
0680     assetAccount = file->account(accountId);
0681     assetSecurity = file->currency(assetAccount.currencyId());
0682     ui->totalAmountEdit->setSharesCommodity(assetSecurity);
0683 }
0684 
0685 void InvestTransactionEditor::Private::scheduleUpdateTotalAmount()
0686 {
0687     QMetaObject::invokeMethod(q, "updateTotalAmount", Qt::QueuedConnection);
0688 }
0689 
0690 void InvestTransactionEditor::Private::setupTabOrder()
0691 {
0692     const auto defaultTabOrder = QStringList{
0693         QLatin1String("activityCombo"),
0694         QLatin1String("dateEdit"),
0695         QLatin1String("securityAccountCombo"),
0696         QLatin1String("sharesAmountEdit"),
0697         QLatin1String("assetAccountCombo"),
0698         QLatin1String("priceAmountEdit"),
0699         QLatin1String("feesCombo"),
0700         QLatin1String("feesAmountEdit"),
0701         QLatin1String("interestCombo"),
0702         QLatin1String("interestAmountEdit"),
0703         QLatin1String("memoEdit"),
0704         QLatin1String("statusCombo"),
0705         QLatin1String("enterButton"),
0706         QLatin1String("cancelButton"),
0707     };
0708     q->setProperty("kmm_defaulttaborder", defaultTabOrder);
0709     q->setProperty("kmm_currenttaborder", q->tabOrder(QLatin1String("investTransactionEditor"), defaultTabOrder));
0710 
0711     q->setupTabOrder(q->property("kmm_currenttaborder").toStringList());
0712 }
0713 
0714 InvestTransactionEditor::InvestTransactionEditor(QWidget* parent, const QString& accId)
0715     : TransactionEditorBase(parent, accId)
0716     , d(new Private(this))
0717 {
0718     d->ui->setupUi(this);
0719 
0720     // initially, the info message is hidden
0721     d->ui->infoMessage->hide();
0722 
0723     d->ui->activityCombo->setModel(d->activitiesModel);
0724 
0725     auto const accountsModel = MyMoneyFile::instance()->accountsModel();
0726 
0727     d->securityFilterModel->addAccountGroup({eMyMoney::Account::Type::Asset});
0728     d->securityFilterModel->setSourceModel(accountsModel);
0729     d->securityFilterModel->setHideEquityAccounts(false);
0730     d->securityFilterModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts());
0731 
0732     d->accountsListModel->setSourceModel(d->securityFilterModel);
0733 
0734     d->securitiesModel->setSourceModel(d->accountsListModel);
0735     d->securitiesModel->setFilterRole(eMyMoney::Model::AccountParentIdRole);
0736     d->securitiesModel->setFilterKeyColumn(0);
0737     d->securitiesModel->setSortRole(Qt::DisplayRole);
0738     d->securitiesModel->setSortLocaleAware(true);
0739     d->securitiesModel->sort(AccountsModel::Column::AccountName);
0740 
0741     d->ui->securityAccountCombo->setModel(d->securitiesModel);
0742     d->ui->securityAccountCombo->lineEdit()->setClearButtonEnabled(true);
0743 
0744     d->accountsModel->addAccountGroup(QVector<eMyMoney::Account::Type> { eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability } );
0745     d->accountsModel->setHideEquityAccounts(false);
0746     d->accountsModel->setSourceModel(accountsModel);
0747     d->accountsModel->sort(AccountsModel::Column::AccountName);
0748     d->ui->assetAccountCombo->setModel(d->accountsModel);
0749     d->ui->assetAccountCombo->setSplitActionVisible(false);
0750 
0751     d->feesModel->addAccountGroup(QVector<eMyMoney::Account::Type> { eMyMoney::Account::Type::Expense });
0752     d->feesModel->setSourceModel(accountsModel);
0753     d->feesModel->sort(AccountsModel::Column::AccountName);
0754     d->ui->feesCombo->setModel(d->feesModel);
0755     d->feeSplitHelper = new KMyMoneyAccountComboSplitHelper(d->ui->feesCombo, d->feeSplitModel);
0756     connect(d->feeSplitHelper, &KMyMoneyAccountComboSplitHelper::accountComboDisabled, d->ui->feesAmountEdit, &AmountEdit::setReadOnly);
0757 
0758     d->interestModel->addAccountGroup(QVector<eMyMoney::Account::Type> { eMyMoney::Account::Type::Income });
0759     d->interestModel->setSourceModel(accountsModel);
0760     d->interestModel->sort(AccountsModel::Column::AccountName);
0761     d->ui->interestCombo->setModel(d->interestModel);
0762     d->interestSplitHelper = new KMyMoneyAccountComboSplitHelper(d->ui->interestCombo, d->interestSplitModel);
0763     connect(d->interestSplitHelper, &KMyMoneyAccountComboSplitHelper::accountComboDisabled, d->ui->interestAmountEdit, &AmountEdit::setReadOnly);
0764 
0765     d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK));
0766     d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel));
0767 
0768     d->ui->statusCombo->setModel(MyMoneyFile::instance()->statusModel());
0769 
0770     d->ui->sharesAmountEdit->setAllowEmpty(true);
0771     d->ui->sharesAmountEdit->setCalculatorButtonVisible(true);
0772 
0773     connect(d->ui->sharesAmountEdit, &AmountEdit::amountChanged, this, [&]() {
0774         if (d->currentActivity) {
0775             d->stockSplit.setShares(d->ui->sharesAmountEdit->value() * d->currentActivity->sharesFactor());
0776             d->stockSplit.setValue(d->currentActivity->valueAllShares().convert(d->transactionCurrency.smallestAccountFraction(), d->security.roundingMethod())
0777                                    * d->currentActivity->sharesFactor());
0778             if (d->currentActivity->type() != eMyMoney::Split::InvestmentTransactionType::SplitShares) {
0779                 d->scheduleUpdateTotalAmount();
0780             }
0781             d->updateWidgetState();
0782         }
0783     });
0784 
0785     d->ui->priceAmountEdit->setAllowEmpty(true);
0786     d->ui->priceAmountEdit->setCalculatorButtonVisible(true);
0787     connect(d->ui->priceAmountEdit, &AmountEdit::amountChanged, this, [&]() {
0788         if (d->currentActivity) {
0789             d->stockSplit.setValue(d->currentActivity->valueAllShares().convert(d->transactionCurrency.smallestAccountFraction(), d->security.roundingMethod())
0790                                    * d->currentActivity->sharesFactor());
0791             if (d->currentActivity->type() != eMyMoney::Split::InvestmentTransactionType::SplitShares) {
0792                 d->scheduleUpdateTotalAmount();
0793             }
0794             d->updateWidgetState();
0795         }
0796     });
0797 
0798     d->ui->feesAmountEdit->setAllowEmpty(true);
0799     d->ui->feesAmountEdit->setCalculatorButtonVisible(true);
0800     connect(d->ui->feesAmountEdit, &AmountEdit::amountChanged, this, [&]() {
0801         d->amountChanged(d->feeSplitModel, d->ui->feesAmountEdit, MyMoneyMoney::ONE);
0802         d->updateWidgetState();
0803         if (!d->ui->feesCombo->getSelected().isEmpty()) {
0804             d->scheduleUpdateTotalAmount();
0805         }
0806     });
0807 
0808     d->ui->interestAmountEdit->setAllowEmpty(true);
0809     d->ui->interestAmountEdit->setCalculatorButtonVisible(true);
0810     connect(d->ui->interestAmountEdit, &AmountEdit::amountChanged, this, [&]() {
0811         d->amountChanged(d->interestSplitModel, d->ui->interestAmountEdit, MyMoneyMoney::MINUS_ONE);
0812         d->updateWidgetState();
0813         if (!d->ui->interestCombo->getSelected().isEmpty()) {
0814             d->scheduleUpdateTotalAmount();
0815         }
0816     });
0817 
0818     WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this);
0819     frameCollection->addFrame(new WidgetHintFrame(d->ui->dateEdit));
0820     frameCollection->addFrame(new WidgetHintFrame(d->ui->securityAccountCombo));
0821     frameCollection->addFrame(new WidgetHintFrame(d->ui->assetAccountCombo));
0822     frameCollection->addFrame(new WidgetHintFrame(d->ui->sharesAmountEdit));
0823     frameCollection->addFrame(new WidgetHintFrame(d->ui->priceAmountEdit));
0824     frameCollection->addFrame(new WidgetHintFrame(d->ui->feesCombo));
0825     frameCollection->addFrame(new WidgetHintFrame(d->ui->feesAmountEdit));
0826     frameCollection->addFrame(new WidgetHintFrame(d->ui->interestCombo));
0827     frameCollection->addFrame(new WidgetHintFrame(d->ui->interestAmountEdit));
0828     frameCollection->addWidget(d->ui->enterButton);
0829 
0830     connect(d->ui->assetAccountCombo, &KMyMoneyAccountCombo::accountSelected, this, [&](const QString& accountId) {
0831         d->setupAssetAccount(accountId);
0832         d->assetSplit.setAccountId(accountId);
0833 
0834         // check the opening dates of this account and
0835         // update the widgets accordingly
0836         d->postdateChanged(d->ui->dateEdit->date());
0837         d->updateWidgetState();
0838     });
0839 
0840     connect(d->ui->dateEdit, &KMyMoneyDateEdit::dateChanged, this, [&](const QDate& date) {
0841         d->postdateChanged(date);
0842     });
0843 
0844     connect(d->ui->activityCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &InvestTransactionEditor::activityChanged);
0845     connect(d->ui->securityAccountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int index) {
0846         const auto idx = d->ui->securityAccountCombo->model()->index(index, 0);
0847         if (idx.isValid()) {
0848             const auto accountId = idx.data(eMyMoney::Model::IdRole).toString();
0849             const auto securityId = idx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0850             try {
0851                 const auto file = MyMoneyFile::instance();
0852                 const auto sec = file->security(securityId);
0853 
0854                 d->stockSplit.setAccountId(accountId);
0855                 d->setSecurity(sec);
0856 
0857                 d->scheduleUpdateTotalAmount();
0858 
0859             } catch (MyMoneyException&) {
0860                 qDebug() << "Problem to find securityId" << accountId << "or" << securityId << "in InvestTransactionEditor::securityAccountChanged";
0861             }
0862         }
0863     });
0864 
0865     connect(d->ui->securityAccountCombo, &QComboBox::currentTextChanged, this, [&](const QString& text) {
0866         if (text.isEmpty() && d->ui->securityAccountCombo->currentIndex() != -1) {
0867             d->ui->securityAccountCombo->setCurrentIndex(-1);
0868             updateWidgets();
0869         }
0870     });
0871 
0872     connect(d->ui->feesCombo, &KMyMoneyAccountCombo::accountSelected, this, [&](const QString& accountId) {
0873         d->categoryChanged(d->feeSplitModel, accountId, d->ui->feesAmountEdit, MyMoneyMoney::ONE);
0874         d->updateWidgetState();
0875         if (!d->feeSplitModel->valueSum().isZero()) {
0876             d->scheduleUpdateTotalAmount();
0877         }
0878     });
0879 
0880     connect(
0881         d->ui->feesCombo,
0882         &KMyMoneyAccountCombo::splitDialogRequest,
0883         this,
0884         [&]() {
0885             d->editSplits(d->feeSplitModel, d->ui->feesAmountEdit, MyMoneyMoney::ONE);
0886         },
0887         Qt::QueuedConnection);
0888 
0889     connect(d->ui->interestCombo, &KMyMoneyAccountCombo::accountSelected, this, [&](const QString& accountId) {
0890         d->categoryChanged(d->interestSplitModel, accountId, d->ui->interestAmountEdit, MyMoneyMoney::MINUS_ONE);
0891         d->updateWidgetState();
0892         if (!d->interestSplitModel->valueSum().isZero()) {
0893             d->scheduleUpdateTotalAmount();
0894         }
0895     });
0896 
0897     connect(
0898         d->ui->interestCombo,
0899         &KMyMoneyAccountCombo::splitDialogRequest,
0900         this,
0901         [&]() {
0902             d->editSplits(d->interestSplitModel, d->ui->interestAmountEdit, MyMoneyMoney::MINUS_ONE);
0903         },
0904         Qt::QueuedConnection);
0905 
0906     connect(d->ui->cancelButton, &QToolButton::clicked, this, [&]() {
0907         Q_EMIT done();
0908     });
0909     connect(d->ui->enterButton, &QToolButton::clicked, this, &InvestTransactionEditor::acceptEdit);
0910 
0911     connect(accountsModel, &QAbstractItemModel::dataChanged, this, [&]() {
0912         d->protectWidgetsForClosedAccount();
0913         d->updateWidgetState();
0914     });
0915 
0916     // handle some events in certain conditions different from default
0917     d->ui->activityCombo->installEventFilter(this);
0918     d->ui->statusCombo->installEventFilter(this);
0919     d->ui->feesCombo->installEventFilter(this);
0920     d->ui->interestCombo->installEventFilter(this);
0921     d->ui->assetAccountCombo->installEventFilter(this);
0922 
0923     d->ui->totalAmountEdit->setCalculatorButtonVisible(false);
0924 
0925     d->setupParentInvestmentAccount(accId);
0926 
0927     setCancelButton(d->ui->cancelButton);
0928     setEnterButton(d->ui->enterButton);
0929 
0930     d->setupTabOrder();
0931 }
0932 
0933 InvestTransactionEditor::~InvestTransactionEditor()
0934 {
0935 }
0936 
0937 void InvestTransactionEditor::updateTotalAmount()
0938 {
0939     // update widget state according to current scenario
0940     d->updateWidgetState();
0941 
0942     if (d->currentActivity) {
0943         const auto totalAmount = d->currentActivity->totalAmount(d->stockSplit, d->feeSplitModel, d->interestSplitModel);
0944         const auto oldValue = d->ui->totalAmountEdit->value();
0945         const auto oldShares = d->ui->totalAmountEdit->shares();
0946 
0947         d->ui->totalAmountEdit->setValue(totalAmount.abs());
0948         d->ui->totalAmountEdit->setValueCommodity(d->transactionCurrency);
0949         d->assetSplit.setValue(-totalAmount);
0950         d->assetSplit.setShares(d->assetSplit.value() / d->assetPrice.rate(d->assetAccount.currencyId()));
0951         d->ui->totalAmountEdit->setShares(d->assetSplit.shares().abs());
0952         // only ask the user for an exchange rate if the value differs from zero
0953         // and the values have changed (reverting in sign does not count as a change)
0954         if (!totalAmount.isZero() && !d->assetSplit.shares().isZero() && !d->bypassUserPriceUpdate) {
0955             if ((oldValue.abs() != d->ui->totalAmountEdit->value().abs()) || (oldShares.abs() != d->ui->totalAmountEdit->shares().abs())) {
0956                 // force display to the transaction commodity so that the values are correct
0957                 d->ui->totalAmountEdit->setDisplayState(AmountEdit::DisplayValue);
0958                 KCurrencyCalculator::updateConversion(d->ui->totalAmountEdit, d->ui->dateEdit->date());
0959                 const auto rate = d->ui->totalAmountEdit->value() / d->ui->totalAmountEdit->shares();
0960                 d->assetPrice =
0961                     MyMoneyPrice(d->transactionCurrency.id(), d->assetAccount.currencyId(), d->transaction.postDate(), rate, QLatin1String("KMyMoney"));
0962                 d->assetSplit.setShares(d->ui->totalAmountEdit->shares());
0963                 // since the total amount is kept as a positive number, we may
0964                 // need to adjust the sign of the shares. The value nevertheless
0965                 // has the correct sign. So if the sign does not match, we
0966                 // simply revert the sign of the shares.
0967                 if (d->assetSplit.shares().isNegative() != d->assetSplit.value().isNegative()) {
0968                     d->assetSplit.setShares(-d->assetSplit.shares());
0969                 }
0970             }
0971         }
0972         // update widget state again, because the conditions may have changed
0973         d->updateWidgetState();
0974     }
0975 }
0976 
0977 
0978 void InvestTransactionEditor::loadTransaction(const QModelIndex& index)
0979 {
0980     // we may also get here during saving the transaction as
0981     // a callback from the model, but we can safely ignore it
0982     // same when we get called from the delegate's setEditorData()
0983     // method
0984     if (accepted() || !index.isValid() || d->loadedFromModel)
0985         return;
0986 
0987     d->loadedFromModel = true;
0988 
0989     d->bypassUserPriceUpdate = true;
0990     d->ui->activityCombo->setCurrentIndex(-1);
0991     d->ui->securityAccountCombo->setCurrentIndex(-1);
0992     const auto file = MyMoneyFile::instance();
0993     auto idx = d->adjustToSecuritySplitIdx(MyMoneyFile::baseModel()->mapToBaseSource(index));
0994     if (!idx.isValid() || idx.data(eMyMoney::Model::IdRole).toString().isEmpty()) {
0995         d->transaction = MyMoneyTransaction();
0996         d->transaction.setCommodity(d->parentAccount.currencyId());
0997         d->transactionCurrency = MyMoneyFile::instance()->baseCurrency();
0998         d->ui->totalAmountEdit->setCommodity(d->transactionCurrency);
0999         d->security = MyMoneySecurity();
1000         d->security.setTradingCurrency(d->transactionCurrency.id());
1001         d->stockSplit = MyMoneySplit();
1002         d->assetSplit = MyMoneySplit();
1003         d->assetAccount = MyMoneyAccount();
1004         d->assetSecurity = MyMoneySecurity();
1005         d->ui->activityCombo->setCurrentIndex(0);
1006         d->ui->securityAccountCombo->setCurrentIndex(-1);
1007         const auto lastUsedPostDate = KMyMoneySettings::lastUsedPostDate();
1008         if (lastUsedPostDate.isValid()) {
1009             d->ui->dateEdit->setDate(lastUsedPostDate.date());
1010         } else {
1011             d->ui->dateEdit->setDate(QDate::currentDate());
1012         }
1013         // select the associated brokerage account if it exists
1014         const auto brokerageAccount = file->accountsModel()->itemByName(d->parentAccount.brokerageName());
1015         d->ui->assetAccountCombo->setSelected(brokerageAccount.id());
1016         d->loadFeeAndInterestAmountEdits();
1017 
1018     } else {
1019         // keep a copy of the transaction and split
1020         d->transaction = file->journalModel()->itemByIndex(idx).transaction();
1021         d->stockSplit = file->journalModel()->itemByIndex(idx).split();
1022 
1023         // during loading the editor the stocksplit object maybe changed which
1024         // don't want here. Therefore, we keep a local copy and reload it
1025         // once needed
1026         const auto stockSplitCopy(d->stockSplit);
1027 
1028         QModelIndex assetAccountSplitIdx;
1029         eMyMoney::Split::InvestmentTransactionType transactionType;
1030 
1031         // KMyMoneyUtils::dissectInvestmentTransaction fills the split models which
1032         // causes to update the widgets when they are not yet setup. So we simply
1033         // prevent sending out signals for them
1034         QSignalBlocker feesModelBlocker(d->feeSplitModel);
1035         QSignalBlocker interestModelBlocker(d->interestSplitModel);
1036 
1037         KMyMoneyUtils::dissectInvestmentTransaction(idx,
1038                                                     assetAccountSplitIdx,
1039                                                     d->feeSplitModel,
1040                                                     d->interestSplitModel,
1041                                                     d->security,
1042                                                     d->transactionCurrency,
1043                                                     transactionType);
1044         d->assetSplit = file->journalModel()->itemByIndex(assetAccountSplitIdx).split();
1045         if (!d->assetSplit.id().isEmpty()) {
1046             d->setupAssetAccount(d->assetSplit.accountId());
1047         }
1048 
1049         // extract conversion rate information for asset split before changing
1050         // the activity because that will need it (in updateTotalAmount() )
1051         if (!(d->assetSplit.shares().isZero() || d->assetSplit.value().isZero())) {
1052             const auto rate = d->assetSplit.value() / d->assetSplit.shares();
1053             d->assetPrice = MyMoneyPrice(d->transactionCurrency.id(), d->assetAccount.currencyId(), d->transaction.postDate(), rate, QLatin1String("KMyMoney"));
1054         }
1055 
1056         // load the widgets. setting activityCombo also initializes
1057         // d->currentActivity to have the right object
1058         d->ui->activityCombo->setCurrentIndex(static_cast<int>(transactionType));
1059 
1060         // changing the transactionType may have modified the stocksplit which is
1061         // not necessary here. To cope with that, we simply reload it from the backup
1062         d->stockSplit = stockSplitCopy;
1063 
1064         d->ui->dateEdit->setDate(d->transaction.postDate());
1065 
1066         d->ui->memoEdit->setPlainText(d->stockSplit.memo());
1067 
1068         d->ui->assetAccountCombo->setSelected(d->assetSplit.accountId());
1069 
1070         d->ui->statusCombo->setCurrentIndex(static_cast<int>(d->stockSplit.reconcileFlag()));
1071 
1072         // Avoid updating other widgets (connected through signal/slot) during loading
1073         const auto indexes = d->securitiesModel->match(d->securitiesModel->index(0,0), eMyMoney::Model::IdRole, d->stockSplit.accountId(), 1, Qt::MatchFixedString);
1074         if (!indexes.isEmpty()) {
1075             d->ui->securityAccountCombo->setCurrentIndex(indexes.first().row());
1076         }
1077 
1078         // changing the security in the last step may have modified the stocksplit
1079         // which is not wanted here. To cope with that, we simply reload it from the model
1080         d->stockSplit = stockSplitCopy;
1081 
1082         // also, setting the security may have changed the precision so we
1083         // reload it here
1084         QSignalBlocker blockShares(d->ui->sharesAmountEdit);
1085         if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares)
1086             d->ui->sharesAmountEdit->setPrecision(-1);
1087         else
1088             d->ui->sharesAmountEdit->setPrecision(MyMoneyMoney::denomToPrec(d->security.smallestAccountFraction()));
1089         d->ui->sharesAmountEdit->setValue(d->stockSplit.shares() * d->currentActivity->sharesFactor());
1090 
1091         d->loadFeeAndInterestAmountEdits();
1092 
1093         d->feeSplitHelper->updateWidget();
1094         d->interestSplitHelper->updateWidget();
1095 
1096         // Avoid updating other widgets (connected through signal/slot) during loading
1097         QSignalBlocker blockPrice(d->ui->priceAmountEdit);
1098         d->currentActivity->loadPriceWidget(d->stockSplit);
1099 
1100         // check if security and amount of shares needs to be
1101         // protected because the security account is closed
1102         d->protectWidgetsForClosedAccount();
1103 
1104         updateTotalAmount();
1105     }
1106 
1107     d->bypassUserPriceUpdate = false;
1108 
1109     // delay update until next run of event loop so that all necessary widgets are visible
1110     QMetaObject::invokeMethod(this, "updateWidgets", Qt::QueuedConnection);
1111 
1112     // set focus to first tab field once we return to event loop
1113     const auto tabOrder = property("kmm_currenttaborder").toStringList();
1114     if (!tabOrder.isEmpty()) {
1115         const auto focusWidget = findChild<QWidget*>(tabOrder.first());
1116         if (focusWidget) {
1117             QMetaObject::invokeMethod(focusWidget, "setFocus", Qt::QueuedConnection);
1118         }
1119     }
1120 }
1121 
1122 void InvestTransactionEditor::updateWidgets()
1123 {
1124     d->updateWidgetState();
1125 }
1126 
1127 void InvestTransactionEditor::activityChanged(int index)
1128 {
1129     const auto type = static_cast<eMyMoney::Split::InvestmentTransactionType>(index);
1130     if (!d->currentActivity || type != d->currentActivity->type()) {
1131         auto oldType = eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType;
1132         if (d->currentActivity) {
1133             oldType = d->currentActivity->type();
1134         }
1135         const auto previousActivity = d->currentActivity;
1136         switch(type) {
1137         default:
1138         case eMyMoney::Split::InvestmentTransactionType::BuyShares:
1139             d->currentActivity = new Invest::Buy(this);
1140             break;
1141         case eMyMoney::Split::InvestmentTransactionType::SellShares:
1142             d->currentActivity = new Invest::Sell(this);
1143             break;
1144         case eMyMoney::Split::InvestmentTransactionType::Dividend:
1145             d->currentActivity = new Invest::Div(this);
1146             break;
1147         case eMyMoney::Split::InvestmentTransactionType::Yield:
1148             d->currentActivity = new Invest::Yield(this);
1149             break;
1150         case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend:
1151             d->currentActivity = new Invest::Reinvest(this);
1152             break;
1153         case eMyMoney::Split::InvestmentTransactionType::AddShares:
1154             d->currentActivity = new Invest::Add(this);
1155             break;
1156         case eMyMoney::Split::InvestmentTransactionType::RemoveShares:
1157             d->currentActivity = new Invest::Remove(this);
1158             break;
1159         case eMyMoney::Split::InvestmentTransactionType::SplitShares:
1160             d->currentActivity = new Invest::Split(this);
1161             break;
1162         case eMyMoney::Split::InvestmentTransactionType::InterestIncome:
1163             d->currentActivity = new Invest::IntInc(this);
1164             break;
1165         }
1166         d->currentActivity->showWidgets();
1167 
1168         // update the stocksplit when switching between e.g. buy and sell
1169         if (previousActivity && previousActivity->sharesFactor() != d->currentActivity->sharesFactor()) {
1170             d->stockSplit.setShares(-d->stockSplit.shares());
1171             d->stockSplit.setValue(-d->stockSplit.value());
1172         }
1173         delete previousActivity;
1174 
1175         if (type == eMyMoney::Split::InvestmentTransactionType::SplitShares && oldType != eMyMoney::Split::InvestmentTransactionType::SplitShares) {
1176             // switch to split
1177             d->stockSplit.setValue(MyMoneyMoney());
1178             d->stockSplit.setPrice(MyMoneyMoney());
1179             d->ui->sharesAmountEdit->setPrecision(-1);
1180         } else if (type != eMyMoney::Split::InvestmentTransactionType::SplitShares && oldType == eMyMoney::Split::InvestmentTransactionType::SplitShares) {
1181             // switch away from split
1182             d->stockSplit.setPrice(d->ui->priceAmountEdit->value());
1183             d->stockSplit.setValue(d->stockSplit.shares() * d->stockSplit.price());
1184             d->ui->sharesAmountEdit->setPrecision(MyMoneyMoney::denomToPrec(d->security.smallestAccountFraction()));
1185         }
1186 
1187         if (d->isDividendOrYield(type) && !d->isDividendOrYield(oldType)) {
1188             // switch to dividend/yield
1189             d->stockSplit.setShares(MyMoneyMoney()); // dividend payments don't affect the number of shares
1190             d->stockSplit.setValue(MyMoneyMoney());
1191             d->stockSplit.setPrice(MyMoneyMoney());
1192         } else if (!d->isDividendOrYield(type) && d->isDividendOrYield(oldType)) {
1193             // switch away from dividend/yield
1194             d->stockSplit.setShares(d->ui->sharesAmountEdit->shares());
1195             d->stockSplit.setPrice(d->ui->priceAmountEdit->value());
1196             d->stockSplit.setValue(d->stockSplit.shares() * d->stockSplit.price());
1197         }
1198 
1199         updateTotalAmount();
1200         d->updateWidgetState();
1201         Q_EMIT editorLayoutChanged();
1202     }
1203 }
1204 
1205 MyMoneyMoney InvestTransactionEditor::totalAmount() const
1206 {
1207     return d->assetSplit.value();
1208 }
1209 
1210 QStringList InvestTransactionEditor::saveTransaction(const QStringList& selectedJournalEntries)
1211 {
1212     MyMoneyTransaction t;
1213 
1214     auto selection(selectedJournalEntries);
1215     connect(MyMoneyFile::instance()->journalModel(), &JournalModel::idChanged, this, [&](const QString& currentId, const QString& previousId) {
1216         selection.replaceInStrings(previousId, currentId);
1217     });
1218 
1219     AlkValue::RoundingMethod roundingMethod = AlkValue::RoundRound;
1220     if (d->security.roundingMethod() != AlkValue::RoundNever)
1221         roundingMethod = d->security.roundingMethod();
1222 
1223     int currencyFraction = d->transactionCurrency.smallestAccountFraction();
1224     int securityFraction = d->security.smallestAccountFraction();
1225 
1226     auto roundSplitValues = [&](MyMoneySplit& split, int sharesFraction) {
1227         split.setShares(MyMoneyMoney(split.shares().convertDenominator(sharesFraction, roundingMethod)));
1228         split.setValue(MyMoneyMoney(split.value().convertDenominator(currencyFraction, roundingMethod)));
1229     };
1230 
1231     if (!d->transaction.id().isEmpty()) {
1232         t = d->transaction;
1233     } else {
1234         // we keep the date when adding a new transaction
1235         // for the next new one
1236         KMyMoneySettings::setLastUsedPostDate(d->ui->dateEdit->date().startOfDay());
1237     }
1238 
1239     d->removeUnusedSplits(t, d->feeSplitModel);
1240     d->removeUnusedSplits(t, d->interestSplitModel);
1241 
1242     // we start with the previous values, clear id to make sure
1243     // we can add them later on
1244     d->stockSplit.clearId();
1245     d->assetSplit.clearId();
1246 
1247     t.setCommodity(d->transactionCurrency.id());
1248 
1249     t.removeSplits();
1250 
1251     t.setPostDate(d->ui->dateEdit->date());
1252     d->stockSplit.setMemo(d->ui->memoEdit->toPlainText());
1253     d->assetSplit.setMemo(d->ui->memoEdit->toPlainText());
1254     d->stockSplit.setAction(d->currentActivity->actionString());
1255 
1256     d->currentActivity->adjustStockSplit(d->stockSplit);
1257 
1258     QList<MyMoneySplit> resultSplits;  // concatenates splits for easy processing
1259 
1260     // now update and add what we have in the models
1261     if (d->currentActivity->assetAccountRequired() != Invest::Activity::Unused) {
1262         d->assetSplit.clearId();
1263         roundSplitValues(d->assetSplit, currencyFraction);
1264         t.addSplit(d->assetSplit);
1265     }
1266 
1267     // Don't do any rounding on a split factor
1268     if (d->currentActivity->type() != eMyMoney::Split::InvestmentTransactionType::SplitShares) {
1269         roundSplitValues(d->stockSplit, securityFraction);
1270         // if there are no shares, we don't have a price either
1271         if (!d->stockSplit.shares().isZero()) {
1272             if (d->currentActivity->priceMode() == eDialogs::PriceMode::PricePerTransaction) {
1273                 d->stockSplit.setPrice(d->stockSplit.value() / d->stockSplit.shares());
1274             } else {
1275                 d->stockSplit.setPrice(d->ui->priceAmountEdit->value());
1276             }
1277         }
1278     }
1279 
1280     if (d->stockSplit.reconcileFlag() != eMyMoney::Split::State::Reconciled && !d->stockSplit.reconcileDate().isValid()
1281         && d->ui->statusCombo->currentIndex() == (int)eMyMoney::Split::State::Reconciled) {
1282         d->stockSplit.setReconcileDate(QDate::currentDate());
1283     }
1284     d->stockSplit.setReconcileFlag(static_cast<eMyMoney::Split::State>(d->ui->statusCombo->currentIndex()));
1285 
1286     t.addSplit(d->stockSplit);
1287 
1288     if (d->currentActivity->feesRequired() != Invest::Activity::Unused) {
1289         resultSplits.append(d->feeSplitModel->splitList());
1290     }
1291     if (d->currentActivity->interestRequired() != Invest::Activity::Unused) {
1292         resultSplits.append(d->interestSplitModel->splitList());
1293     }
1294 
1295     // assuming that all non-stock splits are monetary
1296     for (auto& split : resultSplits) {
1297         split.clearId();
1298         roundSplitValues(split, currencyFraction);
1299         t.addSplit(split);
1300     }
1301 
1302     MyMoneyFileTransaction ft;
1303     try {
1304         const auto file = MyMoneyFile::instance();
1305         if (t.id().isEmpty()) {
1306             file->addTransaction(t);
1307         } else {
1308             t.setImported(false);
1309             file->modifyTransaction(t);
1310         }
1311         ft.commit();
1312 
1313     } catch (const MyMoneyException& e) {
1314         qDebug() << Q_FUNC_INFO << "something went wrong" << e.what();
1315         selection = selectedJournalEntries;
1316     }
1317     return selection;
1318 }
1319 
1320 bool InvestTransactionEditor::eventFilter(QObject* o, QEvent* e)
1321 {
1322     auto cb = qobject_cast<QComboBox*>(o);
1323     if (cb) {
1324         // filter out wheel events for combo boxes if the popup view is not visible
1325         if ((e->type() == QEvent::Wheel) && !cb->view()->isVisible()) {
1326             return true;
1327         }
1328         if (e->type() == QEvent::FocusOut) {
1329             if (o == d->ui->feesCombo) {
1330                 if (needCreateCategory(d->ui->feesCombo)) {
1331                     createCategory(d->ui->feesCombo, eMyMoney::Account::Type::Expense);
1332                 }
1333             } else if (o == d->ui->interestCombo) {
1334                 if (needCreateCategory(d->ui->interestCombo)) {
1335                     createCategory(d->ui->interestCombo, eMyMoney::Account::Type::Income);
1336                 }
1337             } else if (o == d->ui->assetAccountCombo) {
1338                 if (needCreateCategory(d->ui->assetAccountCombo)) {
1339                     createCategory(d->ui->assetAccountCombo, eMyMoney::Account::Type::Asset);
1340                 }
1341             }
1342         }
1343     }
1344     return QWidget::eventFilter(o, e);
1345 }
1346 
1347 void InvestTransactionEditor::setupUi(QWidget* parent)
1348 {
1349     if (d->tabOrderUi == nullptr) {
1350         d->tabOrderUi = new Ui::InvestTransactionEditor;
1351     }
1352     d->tabOrderUi->setupUi(parent);
1353     d->tabOrderUi->infoMessage->setHidden(true);
1354 }
1355 
1356 void InvestTransactionEditor::storeTabOrder(const QStringList& tabOrder)
1357 {
1358     TransactionEditorBase::storeTabOrder(QLatin1String("investTransactionEditor"), tabOrder);
1359 }
1360 
1361 void InvestTransactionEditor::slotSettingsChanged()
1362 {
1363     d->securityFilterModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts());
1364 }
1365 
1366 bool InvestTransactionEditor::isTransactionDataValid() const
1367 {
1368     return d->checkForValidTransaction(false);
1369 }