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