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 }