diff --git a/kmymoney/dialogs/investtransactioneditor.cpp b/kmymoney/dialogs/investtransactioneditor.cpp index 0dd2882a1..55e67a1be 100644 --- a/kmymoney/dialogs/investtransactioneditor.cpp +++ b/kmymoney/dialogs/investtransactioneditor.cpp @@ -1,1196 +1,1227 @@ /* * Copyright 2007-2019 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "investtransactioneditor.h" #include "transactioneditor_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyreconcilecombo.h" #include "kmymoneyactivitycombo.h" #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "investtransaction.h" #include "selectedtransactions.h" #include "transactioneditorcontainer.h" #include "kmymoneycategory.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneyaccountselector.h" #include "kmymoneymvccombo.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "ksplittransactiondlg.h" #include "kcurrencycalculator.h" #include "kmymoneysettings.h" #include "investactivities.h" #include "kmymoneycompletion.h" #include "dialogenums.h" using namespace eMyMoney; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Invest; class InvestTransactionEditorPrivate : public TransactionEditorPrivate { Q_DISABLE_COPY(InvestTransactionEditorPrivate) Q_DECLARE_PUBLIC(InvestTransactionEditor) friend class Invest::Activity; public: explicit InvestTransactionEditorPrivate(InvestTransactionEditor* qq) : TransactionEditorPrivate(qq), m_activity(0), m_phonyAccount(MyMoneyAccount("Phony-ID", MyMoneyAccount())), m_transactionType(eMyMoney::Split::InvestmentTransactionType::BuyShares) { } ~InvestTransactionEditorPrivate() { delete m_activity; } void showCategory(const QString& name, bool visible = true) { Q_Q(InvestTransactionEditor); if (auto cat = dynamic_cast(q->haveWidget(name))) { if (Q_LIKELY(cat->splitButton())) { cat->parentWidget()->setVisible(visible); // show or hide the enclosing QFrame; } else { cat->setVisible(visible); // show or hide the enclosing QFrame; } } } void activityFactory(eMyMoney::Split::InvestmentTransactionType type) { Q_Q(InvestTransactionEditor); if (!m_activity || type != m_activity->type()) { delete m_activity; switch (type) { default: case eMyMoney::Split::InvestmentTransactionType::BuyShares: m_activity = new Buy(q); break; case eMyMoney::Split::InvestmentTransactionType::SellShares: m_activity = new Sell(q); break; case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::Yield: m_activity = new Div(q); break; case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: m_activity = new Reinvest(q); break; case eMyMoney::Split::InvestmentTransactionType::AddShares: m_activity = new Add(q); break; case eMyMoney::Split::InvestmentTransactionType::RemoveShares: m_activity = new Remove(q); break; case eMyMoney::Split::InvestmentTransactionType::SplitShares: m_activity = new Invest::Split(q); break; case eMyMoney::Split::InvestmentTransactionType::InterestIncome: m_activity = new IntInc(q); break; } } } MyMoneyMoney subtotal(const QList& splits) const { MyMoneyMoney sum; foreach (const auto split, splits) sum += split.value(); return sum; } /** * This method creates a transaction to be used for the split fee/interest editor. * It has a reference to a phony account and the splits contained in @a splits . */ bool createPseudoTransaction(MyMoneyTransaction& t, const QList& splits) { t.removeSplits(); MyMoneySplit split; split.setAccountId(m_phonyAccount.id()); split.setValue(-subtotal(splits)); split.setShares(split.value()); t.addSplit(split); m_phonySplit = split; foreach (const auto it_s, splits) { split = it_s; split.clearId(); t.addSplit(split); } return true; } /** * Convenience method used by slotEditInterestSplits() and slotEditFeeSplits(). * * @param categoryWidgetName name of the category widget * @param amountWidgetName name of the amount widget * @param splits the splits that make up the transaction to be edited * @param isIncome @c false for fees, @c true for interest * @param slotEditSplits name of the slot to be connected to the focusIn signal of the * category widget named @p categoryWidgetName in case of multiple splits * in @p splits . */ int editSplits(const QString& categoryWidgetName, const QString& amountWidgetName, QList& splits, bool isIncome, const char* slotEditSplits) { Q_Q(InvestTransactionEditor); int rc = QDialog::Rejected; if (!m_openEditSplits) { // only get in here in a single instance m_openEditSplits = true; // force focus change to update all data auto category = dynamic_cast(m_editWidgets[categoryWidgetName]); if (!category) return rc; QWidget* w = category->splitButton(); if (w) w->setFocus(); auto amount = dynamic_cast(q->haveWidget(amountWidgetName)); if (!amount) return rc; MyMoneyTransaction transaction; transaction.setCommodity(m_currency.id()); if (splits.count() == 0 && !category->selectedItem().isEmpty()) { MyMoneySplit s; s.setAccountId(category->selectedItem()); s.setShares(amount->value()); s.setValue(s.shares()); splits << s; } // use the transactions commodity as the currency indicator for the splits // this is used to allow some useful setting for the fractions in the amount fields try { m_phonyAccount.setCurrencyId(m_transaction.commodity()); m_phonyAccount.fraction(MyMoneyFile::instance()->security(m_transaction.commodity())); } catch (const MyMoneyException &) { qDebug("Unable to setup precision"); } if (createPseudoTransaction(transaction, splits)) { MyMoneyMoney value; QPointer dlg = new KSplitTransactionDlg(transaction, m_phonySplit, m_phonyAccount, false, isIncome, MyMoneyMoney(), m_priceInfo, m_regForm); // q->connect(dlg, SIGNAL(newCategory(MyMoneyAccount&)), q, SIGNAL(newCategory(MyMoneyAccount&))); if ((rc = dlg->exec()) == QDialog::Accepted) { transaction = dlg->transaction(); // collect splits out of the transaction splits.clear(); MyMoneyMoney fees; foreach (const auto split, transaction.splits()) { if (split.accountId() == m_phonyAccount.id()) continue; splits << split; fees += split.shares(); } if (isIncome) fees = -fees; QString categoryId; q->setupCategoryWidget(category, splits, categoryId, slotEditSplits); amount->setValue(fees); q->slotUpdateTotalAmount(); } delete dlg; } // focus jumps into the memo field if ((w = q->haveWidget("memo")) != 0) { w->setFocus(); } m_openEditSplits = false; } return rc; } void updatePriceMode(const MyMoneySplit& split = MyMoneySplit()) { Q_Q(InvestTransactionEditor); if (auto label = dynamic_cast(q->haveWidget("price-label"))) { auto sharesEdit = dynamic_cast(q->haveWidget("shares")); auto priceEdit = dynamic_cast(q->haveWidget("price")); if (!sharesEdit || !priceEdit) return; MyMoneyMoney price; if (!split.id().isEmpty()) price = split.price().reduce(); else price = priceEdit->value().abs(); if (q->priceMode() == eDialogs::PriceMode::PricePerTransaction) { priceEdit->setPrecision(m_currency.pricePrecision()); label->setText(i18n("Transaction amount")); if (!sharesEdit->value().isZero()) priceEdit->setValue(sharesEdit->value().abs() * price); } else if (q->priceMode() == eDialogs::PriceMode::PricePerShare) { priceEdit->setPrecision(m_security.pricePrecision()); label->setText(i18n("Price/Share")); priceEdit->setValue(price); } else priceEdit->setValue(price); } } Activity* m_activity; MyMoneyAccount m_phonyAccount; MyMoneySplit m_phonySplit; MyMoneySplit m_assetAccountSplit; QList m_interestSplits; QList m_feeSplits; MyMoneySecurity m_security; MyMoneySecurity m_currency; eMyMoney::Split::InvestmentTransactionType m_transactionType; }; InvestTransactionEditor::InvestTransactionEditor() : TransactionEditor(*new InvestTransactionEditorPrivate(this)) { Q_D(InvestTransactionEditor); d->m_transactionType = eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType; } InvestTransactionEditor::~InvestTransactionEditor() { } InvestTransactionEditor::InvestTransactionEditor(TransactionEditorContainer* regForm, KMyMoneyRegister::InvestTransaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : TransactionEditor(*new InvestTransactionEditorPrivate(this), regForm, item, list, lastPostDate) { Q_D(InvestTransactionEditor); // after the geometries of the container are updated hide the widgets which are not needed by the current activity connect(d->m_regForm, &TransactionEditorContainer::geometriesUpdated, this, &InvestTransactionEditor::slotTransactionContainerGeometriesUpdated); // dissect the transaction into its type, splits, currency, security etc. KMyMoneyUtils::dissectTransaction(d->m_transaction, d->m_split, d->m_assetAccountSplit, d->m_feeSplits, d->m_interestSplits, d->m_security, d->m_currency, d->m_transactionType); // determine initial activity object d->activityFactory(d->m_transactionType); } void InvestTransactionEditor::createEditWidgets() { Q_D(InvestTransactionEditor); auto activity = new KMyMoneyActivityCombo(); activity->setObjectName("activity"); d->m_editWidgets["activity"] = activity; connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateActivity); connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateButtonState); - d->m_editWidgets["postdate"] = new KMyMoneyDateInput; + auto postDate = d->m_editWidgets["postdate"] = new KMyMoneyDateInput; + connect(postDate, SIGNAL(dateChanged(QDate)), this, SLOT(slotUpdateButtonState())); auto security = new KMyMoneySecurity; security->setObjectName("security"); security->setPlaceholderText(i18n("Security")); d->m_editWidgets["security"] = security; connect(security, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateSecurity); connect(security, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(security, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateSecurity); connect(security, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto asset = new KMyMoneyCategory(false, nullptr); asset->setObjectName("asset-account"); asset->setPlaceholderText(i18n("Asset account")); d->m_editWidgets["asset-account"] = asset; connect(asset, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(asset, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto fees = new KMyMoneyCategory(true, nullptr); fees->setObjectName("fee-account"); fees->setPlaceholderText(i18n("Fees")); d->m_editWidgets["fee-account"] = fees; connect(fees, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateFeeCategory); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(fees, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateFeeCategory); connect(fees, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); connect(fees->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditFeeSplits); auto interest = new KMyMoneyCategory(true, nullptr); interest->setPlaceholderText(i18n("Interest")); interest->setObjectName("interest-account"); d->m_editWidgets["interest-account"] = interest; connect(interest, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateInterestCategory); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(interest, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateInterestCategory); connect(interest, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); connect(interest->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditInterestSplits); auto tag = new KTagContainer; tag->tagCombo()->setPlaceholderText(i18n("Tag")); tag->tagCombo()->setObjectName(QLatin1String("tag")); d->m_editWidgets["tag"] = tag; connect(tag->tagCombo(), &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(tag->tagCombo(), &KMyMoneyMVCCombo::createItem, this, &InvestTransactionEditor::slotNewTag); connect(tag->tagCombo(), &KMyMoneyMVCCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto memo = new KTextEdit; memo->setObjectName("memo"); memo->setTabChangesFocus(true); d->m_editWidgets["memo"] = memo; connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateInvestMemoState); connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_activity->memoText().clear(); d->m_activity->memoChanged() = false; KMyMoneyEdit* value = new KMyMoneyEdit; value->setObjectName("shares"); value->setPlaceholderText(i18n("Shares")); value->setResetButtonVisible(false); d->m_editWidgets["shares"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; value->setObjectName("price"); value->setPlaceholderText(i18n("Price")); value->setResetButtonVisible(false); d->m_editWidgets["price"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; value->setObjectName("fee-amount"); // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["fee-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; value->setObjectName("interest-amount"); // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["interest-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); auto reconcile = new KMyMoneyReconcileCombo; reconcile->setObjectName("reconcile"); d->m_editWidgets["status"] = reconcile; connect(reconcile, &KMyMoneyMVCCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateButtonState); KMyMoneyRegister::QWidgetContainer::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->installEventFilter(this); } QLabel* label; d->m_editWidgets["activity-label"] = label = new QLabel(i18n("Activity")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["postdate-label"] = label = new QLabel(i18n("Date")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["security-label"] = label = new QLabel(i18n("Security")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["shares-label"] = label = new QLabel(i18n("Shares")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["asset-label"] = label = new QLabel(i18n("Account")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["price-label"] = label = new QLabel(i18n("Price/share")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-label"] = label = new QLabel(i18n("Fees")); label->setAlignment(Qt::AlignVCenter); - d->m_editWidgets["fee-amount-label"] = label = new QLabel(""); + d->m_editWidgets["fee-amount-label"] = label = new QLabel(QString()); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-amount-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["memo-label"] = label = new QLabel(i18n("Memo")); label->setAlignment(Qt::AlignVCenter); - d->m_editWidgets["total"] = label = new QLabel(""); + d->m_editWidgets["total"] = label = new QLabel(QString()); label->setAlignment(Qt::AlignVCenter | Qt::AlignRight); d->m_editWidgets["total-label"] = label = new QLabel(i18nc("Total value", "Total")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["status-label"] = label = new QLabel(i18n("Status")); label->setAlignment(Qt::AlignVCenter); // if we don't have more than 1 selected transaction, we don't need // the "don't change" item in some of the combo widgets if (d->m_transactions.count() < 2) { reconcile->removeDontCare(); } } int InvestTransactionEditor::slotEditFeeSplits() { Q_D(InvestTransactionEditor); return d->editSplits("fee-account", "fee-amount", d->m_feeSplits, false, SLOT(slotEditFeeSplits())); } int InvestTransactionEditor::slotEditInterestSplits() { Q_D(InvestTransactionEditor); return d->editSplits("interest-account", "interest-amount", d->m_interestSplits, true, SLOT(slotEditInterestSplits())); } void InvestTransactionEditor::slotCreateSecurity(const QString& name, QString& id) { Q_D(InvestTransactionEditor); MyMoneyAccount acc; QRegExp exp("([^:]+)"); if (exp.indexIn(name) != -1) { acc.setName(exp.cap(1)); slotNewInvestment(acc, d->m_account); // return id id = acc.id(); if (!id.isEmpty()) { slotUpdateSecurity(id); slotReloadEditWidgets(); } } } void InvestTransactionEditor::slotCreateFeeCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); slotNewCategory(acc, MyMoneyFile::instance()->expense()); // return id id = acc.id(); } void InvestTransactionEditor::slotUpdateFeeCategory(const QString& id) { haveWidget("fee-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateInterestCategory(const QString& id) { haveWidget("interest-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotCreateInterestCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); slotNewCategory(acc, MyMoneyFile::instance()->income()); id = acc.id(); } void InvestTransactionEditor::slotReloadEditWidgets() { Q_D(InvestTransactionEditor); auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); auto security = dynamic_cast(haveWidget("security")); if (!interest || !fees || !security) return; AccountSet aSet; QString id; // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); // security aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); } void InvestTransactionEditor::loadEditWidgets(eWidgets::eRegister::Action) { loadEditWidgets(); } void InvestTransactionEditor::loadEditWidgets() { Q_D(InvestTransactionEditor); QString id; auto postDate = dynamic_cast(haveWidget("postdate")); auto reconcile = dynamic_cast(haveWidget("status")); auto security = dynamic_cast(haveWidget("security")); auto activity = dynamic_cast(haveWidget("activity")); auto asset = dynamic_cast(haveWidget("asset-account")); auto memo = dynamic_cast(d->m_editWidgets["memo"]); KMyMoneyEdit* value; auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); if (!postDate || !reconcile || !security || !activity || !asset || !memo || !interest || !fees) return; // check if the current transaction has a reference to an equity account auto haveEquityAccount = false; foreach (const auto split, d->m_transaction.splits()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountType() == Account::Type::Equity) { haveEquityAccount = true; break; } } // asset-account AccountSet aSet; aSet.clear(); aSet.addAccountType(Account::Type::Checkings); aSet.addAccountType(Account::Type::Savings); aSet.addAccountType(Account::Type::Cash); aSet.addAccountType(Account::Type::Asset); aSet.addAccountType(Account::Type::Currency); aSet.addAccountType(Account::Type::CreditCard); if (KMyMoneySettings::expertMode() || haveEquityAccount) aSet.addAccountGroup(Account::Type::Equity); aSet.load(asset->selector()); // security security->setSuppressObjectCreation(false); // allow object creation on the fly aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); // memo memo->setText(d->m_split.memo()); d->m_activity->memoText() = d->m_split.memo(); d->m_activity->memoChanged() = false; if (!isMultiSelection()) { // date if (d->m_transaction.postDate().isValid()) postDate->setDate(d->m_transaction.postDate()); else if (d->m_lastPostDate.isValid()) postDate->setDate(d->m_lastPostDate); else postDate->setDate(QDate::currentDate()); // security (but only if it's not the investment account) if (d->m_split.accountId() != d->m_account.id()) { security->completion()->setSelected(d->m_split.accountId()); security->slotItemSelected(d->m_split.accountId()); } // activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); asset->completion()->setSelected(d->m_assetAccountSplit.accountId()); asset->slotItemSelected(d->m_assetAccountSplit.accountId()); // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); // shares // don't set the value if the number of shares is zero so that // we can see the hint value = dynamic_cast(haveWidget("shares")); if (!value) return; if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) value->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else value->setPrecision(-1); if (!d->m_split.shares().isZero()) value->setValue(d->m_split.shares().abs()); // price d->updatePriceMode(d->m_split); // fee amount value = dynamic_cast(haveWidget("fee-amount")); if (!value) return; value->setValue(d->subtotal(d->m_feeSplits)); // interest amount value = dynamic_cast(haveWidget("interest-amount")); if (!value) return; value->setValue(-d->subtotal(d->m_interestSplits)); // total slotUpdateTotalAmount(); // status if (d->m_split.reconcileFlag() == eMyMoney::Split::State::Unknown) d->m_split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); reconcile->setState(d->m_split.reconcileFlag()); } else { postDate->loadDate(QDate()); reconcile->setState(eMyMoney::Split::State::Unknown); // We don't allow to change the activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); activity->setDisabled(true); // scan the list of selected transactions and check that they have // the same activity. const QString& action = d->m_item->split().action(); bool isNegative = d->m_item->split().shares().isNegative(); bool allSameActivity = true; for (auto it_t = d->m_transactions.begin(); allSameActivity && (it_t != d->m_transactions.end()); ++it_t) { allSameActivity = (action == (*it_t).split().action() && (*it_t).split().shares().isNegative() == isNegative); } QStringList fields; fields << "shares" << "price" << "fee-amount" << "interest-amount"; for (auto it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { value = dynamic_cast(haveWidget((*it_f))); if (!value) return; value->setText(""); value->setAllowEmpty(); } // if we have transactions with different activities, disable some more widgets if (!allSameActivity) { fields << "asset-account" << "fee-account" << "interest-account"; for (auto it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { haveWidget(*it_f)->setDisabled(true); } } } } QWidget* InvestTransactionEditor::firstWidget() const { return nullptr; // let the creator use the first widget in the tab order } bool InvestTransactionEditor::isComplete(QString& reason) const { Q_D(const InvestTransactionEditor); reason.clear(); + + auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); + if (postDate) { + QDate accountOpeningDate = d->m_account.openingDate(); + auto asset = dynamic_cast(haveWidget("asset-account")); + if (asset && asset->isVisible()) { + if (!isMultiSelection() || !asset->currentText().isEmpty()) { + const auto assetId = asset->selectedItem(); + if (!assetId.isEmpty()) { + try { + const auto acc = MyMoneyFile::instance()->account(assetId); + if (acc.openingDate() > accountOpeningDate) + accountOpeningDate = acc.openingDate(); + } catch(MyMoneyException& e) { + qDebug() << "opening date check failed on account" << assetId << e.what(); + } + } + } + } + + if (postDate->date().isValid() && (postDate->date() < accountOpeningDate)) { + postDate->markAsBadDate(true, KMyMoneySettings::schemeColor(SchemeColor::Negative)); + reason = i18n("Cannot enter transaction with postdate prior to account's opening date."); + postDate->setToolTip(reason); + return false; + } + postDate->markAsBadDate(); + postDate->setToolTip(QString()); + } + return d->m_activity->isComplete(reason); } void InvestTransactionEditor::slotUpdateSecurity(const QString& stockId) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); MyMoneyAccount stock = file->account(stockId); d->m_security = file->security(stock.currencyId()); d->m_currency = file->security(d->m_security.tradingCurrency()); bool currencyKnown = !d->m_currency.id().isEmpty(); if (!currencyKnown) { d->m_currency.setTradingSymbol("???"); } else { auto sharesWidget = dynamic_cast(haveWidget("shares")); if (sharesWidget) { if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) sharesWidget->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else sharesWidget->setPrecision(-1); } } d->updatePriceMode(); d->m_activity->preloadAssetAccount(); haveWidget("shares")->setEnabled(currencyKnown); haveWidget("price")->setEnabled(currencyKnown); haveWidget("fee-amount")->setEnabled(currencyKnown); haveWidget("interest-amount")->setEnabled(currencyKnown); slotUpdateTotalAmount(); slotUpdateButtonState(); resizeForm(); } bool InvestTransactionEditor::fixTransactionCommodity(const MyMoneyAccount& /* account */) { return true; } MyMoneyMoney InvestTransactionEditor::totalAmount() const { MyMoneyMoney amount; auto activityCombo = dynamic_cast(haveWidget("activity")); auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); auto feesEdit = dynamic_cast(haveWidget("fee-amount")); auto interestEdit = dynamic_cast(haveWidget("interest-amount")); if (!activityCombo || !sharesEdit || !priceEdit || !feesEdit || !interestEdit) return amount; if (priceMode() == eDialogs::PriceMode::PricePerTransaction) amount = priceEdit->value().abs(); else amount = sharesEdit->value().abs() * priceEdit->value().abs(); if (feesEdit->isVisible()) { MyMoneyMoney fee = feesEdit->value(); MyMoneyMoney factor(-1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: factor = MyMoneyMoney::ONE; break; default: break; } amount += (fee * factor); } if (interestEdit->isVisible()) { MyMoneyMoney interest = interestEdit->value(); MyMoneyMoney factor(1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: factor = MyMoneyMoney::MINUS_ONE; break; default: break; } amount += (interest * factor); } return amount; } void InvestTransactionEditor::slotUpdateTotalAmount() { Q_D(InvestTransactionEditor); auto total = dynamic_cast(haveWidget("total")); if (total && total->isVisible()) { total->setText(totalAmount().convert(d->m_currency.smallestAccountFraction(), d->m_security.roundingMethod()) .formatMoney(d->m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction()))); } } void InvestTransactionEditor::slotTransactionContainerGeometriesUpdated() { Q_D(InvestTransactionEditor); // when the geometries of the transaction container are updated some edit widgets that were // previously hidden are being shown (see QAbstractItemView::updateEditorGeometries) so we // need to update the activity with the current activity in order to show only the widgets // which are needed by the current activity if (d->m_editWidgets.isEmpty()) return; slotUpdateActivity(d->m_activity->type()); } void InvestTransactionEditor::slotUpdateActivity(eMyMoney::Split::InvestmentTransactionType activity) { Q_D(InvestTransactionEditor); // create new activity object if required d->activityFactory(activity); // hide all dynamic widgets d->showCategory("interest-account", false); d->showCategory("fee-account", false); QStringList dynwidgets; dynwidgets << "total-label" << "asset-label" << "fee-label" << "fee-amount-label" << "interest-label" << "interest-amount-label" << "price-label" << "shares-label"; // hiding labels works by clearing them. hide() does not do the job // as the underlying text in the QTable object will shine through QStringList::const_iterator it_s; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QLabel* w = dynamic_cast(haveWidget(*it_s)); if (w) w->setText(QStringLiteral(" ")); } // real widgets can be hidden dynwidgets.clear(); dynwidgets << "asset-account" << "interest-amount" << "fee-amount" << "shares" << "price" << "total"; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QWidget* w = haveWidget(*it_s); if (w) w->hide(); } d->m_activity->showWidgets(); d->m_activity->preloadAssetAccount(); } eDialogs::PriceMode InvestTransactionEditor::priceMode() const { Q_D(const InvestTransactionEditor); eDialogs::PriceMode mode = static_cast(eDialogs::PriceMode::Price); auto sec = dynamic_cast(d->m_editWidgets["security"]); QString accId; if (sec && !sec->currentText().isEmpty()) { accId = sec->selectedItem(); if (accId.isEmpty()) accId = d->m_account.id(); } while (!accId.isEmpty() && mode == eDialogs::PriceMode::Price) { auto acc = MyMoneyFile::instance()->account(accId); if (acc.value("priceMode").isEmpty()) accId = acc.parentAccountId(); else mode = static_cast(acc.value("priceMode").toInt()); } // if mode is still then use that if (mode == eDialogs::PriceMode::Price) mode = eDialogs::PriceMode::PricePerShare; return mode; } MyMoneySecurity InvestTransactionEditor::security() const { Q_D(const InvestTransactionEditor); return d->m_security; } QList InvestTransactionEditor::feeSplits() const { Q_D(const InvestTransactionEditor); return d->m_feeSplits; } QList InvestTransactionEditor::interestSplits() const { Q_D(const InvestTransactionEditor); return d->m_interestSplits; } bool InvestTransactionEditor::setupPrice(const MyMoneyTransaction& t, MyMoneySplit& split) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); auto acc = file->account(split.accountId()); MyMoneySecurity toCurrency(file->security(acc.currencyId())); int fract = acc.fraction(); if (acc.currencyId() != t.commodity()) { if (acc.currencyId().isEmpty()) acc.setCurrencyId(t.commodity()); QMap::Iterator it_p; QString key = t.commodity() + '-' + acc.currencyId(); it_p = d->m_priceInfo.find(key); // if it's not found, then collect it from the user first MyMoneyMoney price; if (it_p == d->m_priceInfo.end()) { MyMoneySecurity fromCurrency = file->security(t.commodity()); MyMoneyMoney fromValue, toValue; fromValue = split.value(); const MyMoneyPrice &priceInfo = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id(), t.postDate()); toValue = split.value() * priceInfo.rate(toCurrency.id()); QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, t.postDate(), fract, d->m_regForm); if (calc->exec() == QDialog::Rejected) { delete calc; return false; } price = calc->price(); delete calc; d->m_priceInfo[key] = price; } else { price = (*it_p); } // update shares if the transaction commodity is the currency // of the current selected account split.setShares(split.value() * price); } else { split.setShares(split.value()); } return true; } bool InvestTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyMoneyTransaction& torig, const MyMoneySplit& sorig, bool /* skipPriceDialog */) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); // we start with the previous values, make sure we can add them later on t = torig; MyMoneySplit s0 = sorig; s0.clearId(); auto sec = dynamic_cast(d->m_editWidgets["security"]); if (sec && (!isMultiSelection() || !sec->currentText().isEmpty())) { QString securityId = sec->selectedItem(); if (!securityId.isEmpty()) { s0.setAccountId(securityId); MyMoneyAccount stockAccount = file->account(securityId); QString currencyId = stockAccount.currencyId(); MyMoneySecurity security = file->security(currencyId); t.setCommodity(security.tradingCurrency()); } else { s0.setAccountId(d->m_account.id()); t.setCommodity(d->m_account.currencyId()); } } // extract price info from original transaction d->m_priceInfo.clear(); if (!torig.id().isEmpty()) { foreach (const auto split, torig.splits()) { if (split.id() != sorig.id()) { auto cat = file->account(split.accountId()); if (cat.currencyId() != d->m_account.currencyId()) { if (cat.currencyId().isEmpty()) cat.setCurrencyId(d->m_account.currencyId()); if (!split.shares().isZero() && !split.value().isZero()) { d->m_priceInfo[cat.currencyId()] = (split.shares() / split.value()).reduce(); } } } } } t.removeSplits(); auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate && postDate->date().isValid()) { t.setPostDate(postDate->date()); } // memo and number field are special: if we have multiple transactions selected // and the edit field is empty, we treat it as "not modified". // FIXME a better approach would be to have a 'dirty' flag with the widgets // which identifies if the originally loaded value has been modified // by the user auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { if (!isMultiSelection() || d->m_activity->memoChanged()) s0.setMemo(memo->toPlainText()); } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency = file->security(t.commodity()); eMyMoney::Split::InvestmentTransactionType transactionType; // extract the splits from the original transaction, but only // if there is one because otherwise the currency is overridden if (t.commodity().isEmpty()) { KMyMoneyUtils::dissectTransaction(torig, sorig, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); } // check if the trading currency is the same if the security has changed // in case it differs, check that we have a price (request from user) // and convert all splits // TODO // do the conversions here // TODO // keep the current activity object and create a new one // that can be destroyed later on auto activity = d->m_activity; d->m_activity = 0; // make sure we create a new one d->activityFactory(activity->type()); // if the activity is not set in the combo widget, we keep // the one which is used in the original transaction auto activityCombo = dynamic_cast(haveWidget("activity")); if (activityCombo && activityCombo->activity() == eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType) { d->activityFactory(transactionType); } // if we mark the split reconciled here, we'll use today's date if no reconciliation date is given auto status = dynamic_cast(d->m_editWidgets["status"]); if (status && status->state() != eMyMoney::Split::State::Unknown) s0.setReconcileFlag(status->state()); if (s0.reconcileFlag() == eMyMoney::Split::State::Reconciled && !s0.reconcileDate().isValid()) s0.setReconcileDate(QDate::currentDate()); // call the creation logic for the current selected activity bool rc = d->m_activity->createTransaction(t, s0, assetAccountSplit, feeSplits, d->m_feeSplits, interestSplits, d->m_interestSplits, security, currency); // now switch back to the original activity delete d->m_activity; d->m_activity = activity; // add the splits to the transaction if (rc) { if (security.name().isEmpty()) // new transaction has no security filled... security = file->security(file->account(s0.accountId()).currencyId()); // ...so fetch it from s0 split QList resultSplits; // concatenates splits for easy processing if (!assetAccountSplit.accountId().isEmpty()) resultSplits.append(assetAccountSplit); if (!feeSplits.isEmpty()) resultSplits.append(feeSplits); if (!interestSplits.isEmpty()) resultSplits.append(interestSplits); AlkValue::RoundingMethod roundingMethod = AlkValue::RoundRound; if (security.roundingMethod() != AlkValue::RoundNever) roundingMethod = security.roundingMethod(); int currencyFraction = currency.smallestAccountFraction(); int securityFraction = security.smallestAccountFraction(); // assuming that all non-stock splits are monetary foreach (auto split, resultSplits) { split.clearId(); split.setShares(MyMoneyMoney(split.shares().convertDenominator(currencyFraction, roundingMethod))); split.setValue(MyMoneyMoney(split.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(split); } // Don't do any rounding on a split factor if (d->m_activity->type() != eMyMoney::Split::InvestmentTransactionType::SplitShares) { s0.setShares(MyMoneyMoney(s0.shares().convertDenominator(securityFraction, roundingMethod))); // only shares variable from stock split isn't evaluated in currency s0.setValue(MyMoneyMoney(s0.value().convertDenominator(currencyFraction, roundingMethod))); } t.addSplit(s0); } return rc; } void InvestTransactionEditor::setupFinalWidgets() { addFinalWidget(haveWidget("memo")); } void InvestTransactionEditor::slotUpdateInvestMemoState() { Q_D(InvestTransactionEditor); auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_activity->memoChanged() = (memo->toPlainText() != d->m_activity->memoText()); } } diff --git a/kmymoney/dialogs/stdtransactioneditor.cpp b/kmymoney/dialogs/stdtransactioneditor.cpp index 076701b0b..fb499c476 100644 --- a/kmymoney/dialogs/stdtransactioneditor.cpp +++ b/kmymoney/dialogs/stdtransactioneditor.cpp @@ -1,1670 +1,1670 @@ /* * Copyright 2009-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "stdtransactioneditor.h" #include "transactioneditor_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyreconcilecombo.h" #include "kmymoneycashflowcombo.h" #include "kmymoneypayeecombo.h" #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "tabbar.h" #include "kmymoneycategory.h" #include "kmymoneymvccombo.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneylineedit.h" #include "kmymoneyaccountselector.h" #include "mymoneyfile.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "kmymoneyutils.h" #include "kmymoneycompletion.h" #include "transaction.h" #include "transactionform.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "transactioneditorcontainer.h" #include "ksplittransactiondlg.h" #include "kcurrencycalculator.h" #include "kselecttransactionsdlg.h" #include "widgetenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; class StdTransactionEditorPrivate : public TransactionEditorPrivate { Q_DISABLE_COPY(StdTransactionEditorPrivate) public: explicit StdTransactionEditorPrivate(StdTransactionEditor *qq) : TransactionEditorPrivate(qq), m_inUpdateVat(false) { } ~StdTransactionEditorPrivate() { } MyMoneyMoney m_shares; bool m_inUpdateVat; }; StdTransactionEditor::StdTransactionEditor() : TransactionEditor(*new StdTransactionEditorPrivate(this)) { } StdTransactionEditor::StdTransactionEditor(TransactionEditorContainer* regForm, KMyMoneyRegister::Transaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : TransactionEditor(*new StdTransactionEditorPrivate(this), regForm, item, list, lastPostDate) { } StdTransactionEditor::~StdTransactionEditor() { } void StdTransactionEditor::createEditWidgets() { Q_D(StdTransactionEditor); // we only create the account widget in case it is needed // to avoid confusion in the tab order later on. if (d->m_item->showRowInForm(0)) { auto account = new KMyMoneyCategory; account->setPlaceholderText(i18n("Account")); account->setObjectName(QLatin1String("Account")); d->m_editWidgets["account"] = account; connect(account, &QComboBox::editTextChanged, this, &StdTransactionEditor::slotUpdateButtonState); connect(account, &KMyMoneyCombo::itemSelected, this, &StdTransactionEditor::slotUpdateAccount); } auto payee = new KMyMoneyPayeeCombo; payee->setPlaceholderText(i18n("Payer/Receiver")); payee->setObjectName(QLatin1String("Payee")); d->m_editWidgets["payee"] = payee; connect(payee, &KMyMoneyMVCCombo::createItem, this, &StdTransactionEditor::slotNewPayee); connect(payee, &KMyMoneyMVCCombo::objectCreation, this, &StdTransactionEditor::objectCreation); connect(payee, &KMyMoneyMVCCombo::itemSelected, this, &StdTransactionEditor::slotUpdatePayee); connect(payee, &QComboBox::editTextChanged, this, &StdTransactionEditor::slotUpdateButtonState); auto category = new KMyMoneyCategory(true, nullptr); category->setPlaceholderText(i18n("Category/Account")); category->setObjectName(QLatin1String("Category/Account")); d->m_editWidgets["category"] = category; connect(category, &KMyMoneyCombo::itemSelected, this, &StdTransactionEditor::slotUpdateCategory); connect(category, &QComboBox::editTextChanged, this, &StdTransactionEditor::slotUpdateButtonState); connect(category, &KMyMoneyCombo::createItem, this, &StdTransactionEditor::slotCreateCategory); connect(category, &KMyMoneyCombo::objectCreation, this, &StdTransactionEditor::objectCreation); connect(category->splitButton(), &QAbstractButton::clicked, this, &StdTransactionEditor::slotEditSplits); // initially disable the split button since we don't have an account set if (category->splitButton()) category->splitButton()->setDisabled(d->m_account.id().isEmpty()); auto tag = new KTagContainer; tag->tagCombo()->setPlaceholderText(i18n("Tag")); tag->tagCombo()->setObjectName(QLatin1String("Tag")); d->m_editWidgets["tag"] = tag; connect(tag->tagCombo(), &KMyMoneyMVCCombo::createItem, this, &StdTransactionEditor::slotNewTag); connect(tag->tagCombo(), &KMyMoneyMVCCombo::objectCreation, this, &StdTransactionEditor::objectCreation); auto memo = new KTextEdit; memo->setObjectName(QLatin1String("Memo")); memo->setTabChangesFocus(true); connect(memo, &QTextEdit::textChanged, this, &StdTransactionEditor::slotUpdateMemoState); connect(memo, &QTextEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); d->m_editWidgets["memo"] = memo; d->m_memoText.clear(); d->m_memoChanged = false; bool showNumberField = true; switch (d->m_account.accountType()) { case eMyMoney::Account::Type::Savings: case eMyMoney::Account::Type::Cash: case eMyMoney::Account::Type::Loan: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: showNumberField = KMyMoneySettings::alwaysShowNrField(); break; case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: showNumberField = false; break; default: break; } if (showNumberField) { auto number = new KMyMoneyLineEdit; number->setPlaceholderText(i18n("Number")); number->setObjectName(QLatin1String("Number")); d->m_editWidgets["number"] = number; connect(number, &KMyMoneyLineEdit::lineChanged, this, &StdTransactionEditor::slotNumberChanged); // number->installEventFilter(this); } auto postDate = new KMyMoneyDateInput; d->m_editWidgets["postdate"] = postDate; postDate->setObjectName(QLatin1String("PostDate")); connect(postDate, &KMyMoneyDateInput::dateChanged, this, &StdTransactionEditor::slotUpdateButtonState); postDate->setDate(QDate()); auto value = new KMyMoneyEdit; d->m_editWidgets["amount"] = value; value->setObjectName(QLatin1String("Amount")); value->setResetButtonVisible(false); connect(value, &KMyMoneyEdit::valueChanged, this, &StdTransactionEditor::slotUpdateAmount); connect(value, &KMyMoneyEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); value = new KMyMoneyEdit; d->m_editWidgets["payment"] = value; value->setObjectName(QLatin1String("Payment")); value->setResetButtonVisible(false); connect(value, &KMyMoneyEdit::valueChanged, this, &StdTransactionEditor::slotUpdatePayment); connect(value, &KMyMoneyEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); value = new KMyMoneyEdit; d->m_editWidgets["deposit"] = value; value->setObjectName(QLatin1String("Deposit")); value->setResetButtonVisible(false); connect(value, &KMyMoneyEdit::valueChanged, this, &StdTransactionEditor::slotUpdateDeposit); connect(value, &KMyMoneyEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); auto cashflow = new KMyMoneyCashFlowCombo(d->m_account.accountGroup(), nullptr); d->m_editWidgets["cashflow"] = cashflow; cashflow->setObjectName(QLatin1String("Cashflow")); connect(cashflow, &KMyMoneyCashFlowCombo::directionSelected, this, &StdTransactionEditor::slotUpdateCashFlow); auto reconcile = new KMyMoneyReconcileCombo; d->m_editWidgets["status"] = reconcile; reconcile->setObjectName(QLatin1String("Reconcile")); KMyMoneyRegister::QWidgetContainer::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->installEventFilter(this); } // if we don't have more than 1 selected transaction, we don't need // the "don't change" item in some of the combo widgets if (!isMultiSelection()) { reconcile->removeDontCare(); cashflow->removeDontCare(); } QLabel* label; d->m_editWidgets["account-label"] = label = new QLabel(i18n("Account")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["category-label"] = label = new QLabel(i18n("Category")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["tag-label"] = label = new QLabel(i18n("Tags")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["memo-label"] = label = new QLabel(i18n("Memo")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["number-label"] = label = new QLabel(i18n("Number")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["date-label"] = label = new QLabel(i18n("Date")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["amount-label"] = label = new QLabel(i18n("Amount")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["status-label"] = label = new QLabel(i18n("Status")); label->setAlignment(Qt::AlignVCenter); // create a copy of tabbar above the form (if we are created for a form) auto form = dynamic_cast(d->m_regForm); if (form) { auto tabbar = new KMyMoneyTransactionForm::TabBar; d->m_editWidgets["tabbar"] = tabbar; tabbar->setObjectName(QLatin1String("TabBar")); tabbar->copyTabs(form->getTabBar()); connect(tabbar, &KMyMoneyTransactionForm::TabBar::tabCurrentChanged, this, &StdTransactionEditor::slotUpdateAction); connect(tabbar, &KMyMoneyTransactionForm::TabBar::tabCurrentChanged, this, &TransactionEditor::operationTypeChanged); } setupPrecision(); } void StdTransactionEditor::setupCategoryWidget(QString& categoryId) { Q_D(StdTransactionEditor); if (auto categoryWidget = dynamic_cast(d->m_editWidgets["category"])) TransactionEditor::setupCategoryWidget(categoryWidget, d->m_splits, categoryId, SLOT(slotEditSplits())); if (d->m_splits.count() == 1) d->m_shares = d->m_splits[0].shares(); } bool StdTransactionEditor::isTransfer(const QString& accId1, const QString& accId2) const { if (accId1.isEmpty() || accId2.isEmpty()) return false; return MyMoneyFile::instance()->account(accId1).isIncomeExpense() == MyMoneyFile::instance()->account(accId2).isIncomeExpense(); } void StdTransactionEditor::loadEditWidgets(eRegister::Action action) { Q_D(StdTransactionEditor); // don't kick off VAT processing from here d->m_inUpdateVat = true; QWidget* w; AccountSet aSet; // load the account widget if (auto account = dynamic_cast(haveWidget("account"))) { aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.removeAccountType(eMyMoney::Account::Type::AssetLoan); aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep); aSet.removeAccountType(eMyMoney::Account::Type::Investment); aSet.removeAccountType(eMyMoney::Account::Type::Stock); aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket); aSet.removeAccountType(eMyMoney::Account::Type::Loan); aSet.load(account->selector()); account->completion()->setSelected(d->m_account.id()); account->slotItemSelected(d->m_account.id()); } // load the payee widget auto payee = dynamic_cast(d->m_editWidgets["payee"]); if (payee) payee->loadPayees(MyMoneyFile::instance()->payeeList()); // load the category widget auto category = dynamic_cast(d->m_editWidgets["category"]); if (category) disconnect(category, &KMyMoneyCategory::focusIn, this, &StdTransactionEditor::slotEditSplits); // load the tag widget //auto tag = dynamic_cast(m_editWidgets["tag"]); auto tag = dynamic_cast(d->m_editWidgets["tag"]); if (tag) tag->loadTags(MyMoneyFile::instance()->tagList()); // check if the current transaction has a reference to an equity account auto haveEquityAccount = false; foreach (const auto split, d->m_transaction.splits()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountType() == eMyMoney::Account::Type::Equity) { haveEquityAccount = true; break; } } aSet.clear(); aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.addAccountGroup(eMyMoney::Account::Type::Income); aSet.addAccountGroup(eMyMoney::Account::Type::Expense); if (KMyMoneySettings::expertMode() || haveEquityAccount) aSet.addAccountGroup(eMyMoney::Account::Type::Equity); aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep); aSet.removeAccountType(eMyMoney::Account::Type::Investment); aSet.removeAccountType(eMyMoney::Account::Type::Stock); aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket); if (category) aSet.load(category->selector()); // if an account is specified then remove it from the widget so that the user // cannot create a transfer with from and to account being the same account if (!d->m_account.id().isEmpty()) category->selector()->removeItem(d->m_account.id()); // also show memo text if isMultiSelection() if (auto memoWidget = dynamic_cast(d->m_editWidgets["memo"])) memoWidget->setText(d->m_split.memo()); // need to know if it changed d->m_memoText = d->m_split.memo(); d->m_memoChanged = false; if (!isMultiSelection()) { if (auto dateWidget = dynamic_cast(d->m_editWidgets["postdate"])) { if (d->m_transaction.postDate().isValid()) dateWidget->setDate(d->m_transaction.postDate()); else if (d->m_lastPostDate.isValid()) dateWidget->setDate(d->m_lastPostDate); else dateWidget->setDate(QDate::currentDate()); } if ((w = haveWidget("number")) != 0) { if (auto lineEdit = dynamic_cast(w)) lineEdit->loadText(d->m_split.number()); if (d->m_transaction.id().isEmpty() // new transaction && dynamic_cast(w)->text().isEmpty() // no number filled in && d->m_account.accountType() == eMyMoney::Account::Type::Checkings // checkings account && KMyMoneySettings::autoIncCheckNumber() // and auto inc number turned on? && action != eRegister::Action::Deposit // only transfers or withdrawals && d->m_paymentMethod == eMyMoney::Schedule::PaymentType::WriteChecque) {// only for WriteChecque assignNextNumber(); } } if (auto statusWidget = dynamic_cast(d->m_editWidgets["status"])) statusWidget->setState(d->m_split.reconcileFlag()); QString payeeId = d->m_split.payeeId(); if (payee && !payeeId.isEmpty()) payee->setSelectedItem(payeeId); QList t = d->m_split.tagIdList(); if (tag && !t.isEmpty()) for (auto i = 0; i < t.size(); ++i) tag->addTagWidget(t[i]); d->m_splits.clear(); if (d->m_transaction.splitCount() < 2) { category->completion()->setSelected(QString()); } else { foreach (const auto split, d->m_transaction.splits()) { if (split == d->m_split) continue; d->m_splits.append(split); } } QString categoryId; setupCategoryWidget(categoryId); if ((w = haveWidget("cashflow")) != 0) { if (auto cashflow = dynamic_cast(w)) cashflow->setDirection(!d->m_split.value().isPositive() ? eRegister::CashFlowDirection::Payment : eRegister::CashFlowDirection::Deposit); // include isZero case } if ((w = haveWidget("category-label")) != 0) { if (auto categoryLabel = dynamic_cast(w)) { if (isTransfer(d->m_split.accountId(), categoryId)) { if (d->m_split.value().isPositive()) categoryLabel->setText(i18n("Transfer from")); else categoryLabel->setText(i18n("Transfer to")); } } } MyMoneyMoney value = d->m_split.shares(); if (haveWidget("deposit")) { auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"]); auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"]); if (depositWidget && paymentWidget) { if (d->m_split.shares().isNegative()) { depositWidget->loadText(QString()); paymentWidget->setValue(value.abs()); } else { depositWidget->setValue(value.abs()); paymentWidget->loadText(QString()); } } } if ((w = haveWidget("amount")) != 0) { if (auto amountWidget = dynamic_cast(w)) amountWidget->setValue(value.abs()); } slotUpdateCategory(categoryId); // try to preset for specific action if a new transaction is being started if (d->m_transaction.id().isEmpty()) { if ((w = haveWidget("category-label")) != 0) { auto tabbar = dynamic_cast(haveWidget("tabbar")); if (action == eRegister::Action::None) { if (tabbar) { action = static_cast(tabbar->currentIndex()); } } if (action != eRegister::Action::None) { if (auto categoryLabel = dynamic_cast(w)) { if (action == eRegister::Action::Transfer) { if (d->m_split.value().isPositive()) categoryLabel->setText(i18n("Transfer from")); else categoryLabel->setText(i18n("Transfer to")); } } if ((w = haveWidget("cashflow")) != 0) { if (auto cashflow = dynamic_cast(w)) { if (action == eRegister::Action::Deposit || (action == eRegister::Action::Transfer && d->m_split.value().isPositive())) cashflow->setDirection(eRegister::CashFlowDirection::Deposit); else cashflow->setDirection(eRegister::CashFlowDirection::Payment); } } if (tabbar) { tabbar->setCurrentIndex((int)action); } } } } else { if (auto tabbar = dynamic_cast(haveWidget("tabbar"))) { if (!isTransfer(d->m_split.accountId(), categoryId)) tabbar->setCurrentIndex(d->m_split.value().isNegative() ? (int)eRegister::Action::Withdrawal : (int)eRegister::Action::Deposit); else tabbar->setCurrentIndex((int)eRegister::Action::Transfer); } } } else { // isMultiSelection() if (auto postDateWidget = dynamic_cast(d->m_editWidgets["postdate"])) postDateWidget->loadDate(QDate()); if (auto statusWidget = dynamic_cast(d->m_editWidgets["status"])) statusWidget->setState(eMyMoney::Split::State::Unknown); if (haveWidget("deposit")) { if (auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"])) { depositWidget->loadText(QString()); depositWidget->setAllowEmpty(); } if (auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"])) { paymentWidget->loadText(QString()); paymentWidget->setAllowEmpty(); } } if ((w = haveWidget("amount")) != 0) { if (auto amountWidget = dynamic_cast(w)) { amountWidget->loadText(QString()); amountWidget->setAllowEmpty(); } } slotUpdateAction((int)action); if ((w = haveWidget("tabbar")) != 0) { w->setEnabled(false); } if (category && category->completion()) category->completion()->setSelected(QString()); } // allow kick off VAT processing again d->m_inUpdateVat = false; } void StdTransactionEditor::loadEditWidgets() { loadEditWidgets(eRegister::Action::None); } QWidget* StdTransactionEditor::firstWidget() const { Q_D(const StdTransactionEditor); QWidget* w = nullptr; if (d->m_initialAction != eRegister::Action::None) { w = haveWidget("payee"); } return w; } void StdTransactionEditor::slotReloadEditWidgets() { Q_D(StdTransactionEditor); // reload category widget if (auto category = dynamic_cast(d->m_editWidgets["category"])) { QString categoryId = category->selectedItem(); AccountSet aSet; aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.addAccountGroup(eMyMoney::Account::Type::Income); aSet.addAccountGroup(eMyMoney::Account::Type::Expense); if (KMyMoneySettings::expertMode()) aSet.addAccountGroup(eMyMoney::Account::Type::Equity); aSet.load(category->selector()); // if an account is specified then remove it from the widget so that the user // cannot create a transfer with from and to account being the same account if (!d->m_account.id().isEmpty()) category->selector()->removeItem(d->m_account.id()); if (!categoryId.isEmpty()) category->setSelectedItem(categoryId); } // reload payee widget if (auto payee = dynamic_cast(d->m_editWidgets["payee"])) { QString payeeId = payee->selectedItem(); payee->loadPayees(MyMoneyFile::instance()->payeeList()); if (!payeeId.isEmpty()) { payee->setSelectedItem(payeeId); } } // reload tag widget if (auto tag = dynamic_cast(d->m_editWidgets["tag"])) { QString tagId = tag->tagCombo()->selectedItem(); tag->loadTags(MyMoneyFile::instance()->tagList()); if (!tagId.isEmpty()) { tag->RemoveAllTagWidgets(); tag->addTagWidget(tagId); } } } void StdTransactionEditor::slotUpdatePayee(const QString& payeeId) { // in case of an empty payee, there is nothing to do if (payeeId.isEmpty()) return; Q_D(StdTransactionEditor); // we have a new payee assigned to this transaction. // in case there is no category assigned, no value entered and no // memo available, we search for the last transaction of this payee // in the account. if (d->m_transaction.id().isEmpty() && d->m_splits.count() == 0 && KMyMoneySettings::autoFillTransaction() != 0) { // check if category is empty if (auto category = dynamic_cast(d->m_editWidgets["category"])) { QStringList list; category->selectedItems(list); if (!list.isEmpty()) return; } // check if memo is empty auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo && !memo->toPlainText().isEmpty()) return; // check if all value fields are empty QStringList fields; fields << "amount" << "payment" << "deposit"; QStringList::const_iterator it_f; for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { const auto amount = dynamic_cast(haveWidget(*it_f)); if (amount && !amount->value().isZero()) return; } #if 0 // Tony mentioned, that autofill does not work when he changed the date. Well, // that certainly makes sense when you enter transactions in register mode as // opposed to form mode, because the date field is located prior to the date // field in the tab order of the widgets and the user might have already // changed it. // // So I commented out the code that checks the date but left it in for reference. // (ipwizard, 2008-04-07) // check if date has been altered by user auto postDate = dynamic_cast(m_editWidgets["postdate"]); if (postDate && (m_lastPostDate.isValid() && (postDate->date() != m_lastPostDate)) || (!m_lastPostDate.isValid() && (postDate->date() != QDate::currentDate()))) return; #endif // if we got here, we have to autofill autoFill(payeeId); } // If payee has associated default account (category), set that now. const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeId); if (payeeObj.defaultAccountEnabled()) { if (auto category = dynamic_cast(d->m_editWidgets["category"])) category->slotItemSelected(payeeObj.defaultAccountId()); } } MyMoneyMoney StdTransactionEditor::shares(const MyMoneyTransaction& t) const { Q_D(const StdTransactionEditor); MyMoneyMoney result; foreach (const auto split, t.splits()) { if (split.accountId() == d->m_account.id()) { result += split.shares(); } } return result; } struct uniqTransaction { const MyMoneyTransaction* t; int cnt; }; void StdTransactionEditor::autoFill(const QString& payeeId) { Q_D(StdTransactionEditor); QList > list; MyMoneyTransactionFilter filter(d->m_account.id()); filter.addPayee(payeeId); MyMoneyFile::instance()->transactionList(list, filter); if (!list.empty()) { // ok, we found at least one previous transaction. now we clear out // what we have collected so far and add those splits from // the previous transaction. QList >::const_iterator it_t; QMap uniqList; // collect the transactions and see if we have any duplicates for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { QString key = (*it_t).first.accountSignature(); int cnt = 0; QMap::iterator it_u; do { QString ukey = QString("%1-%2").arg(key).arg(cnt); it_u = uniqList.find(ukey); if (it_u == uniqList.end()) { uniqList[ukey].t = &((*it_t).first); uniqList[ukey].cnt = 1; } else if (KMyMoneySettings::autoFillTransaction() == 1) { // we already have a transaction with this signature. we must // now check, if we should really treat it as a duplicate according // to the value comparison delta. MyMoneyMoney s1 = shares(*((*it_u).t)); MyMoneyMoney s2 = shares((*it_t).first); if (s2.abs() > s1.abs()) { MyMoneyMoney t(s1); s1 = s2; s2 = t; } MyMoneyMoney diff; if (s2.isZero()) diff = s1.abs(); else diff = ((s1 - s2) / s2).convert(10000); if (diff.isPositive() && diff <= MyMoneyMoney(KMyMoneySettings::autoFillDifference(), 100)) { uniqList[ukey].t = &((*it_t).first); break; // end while loop } } else if (KMyMoneySettings::autoFillTransaction() == 2) { (*it_u).cnt++; break; // end while loop } ++cnt; } while (it_u != uniqList.end()); } MyMoneyTransaction t; if (KMyMoneySettings::autoFillTransaction() != 2) { #if 0 // I removed this code to allow cancellation of an autofill if // it does not match even if there is only a single matching // transaction for the payee in question. In case, we want to revert // to the old behavior, don't forget to uncomment the closing // brace further down in the code as well. (ipwizard 2009-01-16) if (uniqList.count() == 1) { t = list.last().first; } else { #endif QPointer dlg = new KSelectTransactionsDlg(d->m_account, d->m_regForm); dlg->setWindowTitle(i18n("Select autofill transaction")); QMap::const_iterator it_u; for (it_u = uniqList.constBegin(); it_u != uniqList.constEnd(); ++it_u) { dlg->addTransaction(*(*it_u).t); } auto tRegister = dlg->getRegister(); // setup sort order tRegister->setSortOrder("1,-9,-4"); // sort the transactions according to the sort setting tRegister->sortItems(); // and select the last item if (tRegister->lastItem()) tRegister->selectItem(tRegister->lastItem()); if (dlg->exec() == QDialog::Accepted) { t = dlg->transaction(); } #if 0 } #endif } else { int maxCnt = 0; QMap::const_iterator it_u; for (it_u = uniqList.constBegin(); it_u != uniqList.constEnd(); ++it_u) { if ((*it_u).cnt > maxCnt) { t = *(*it_u).t; maxCnt = (*it_u).cnt; } } } if (t != MyMoneyTransaction()) { d->m_transaction.removeSplits(); d->m_split = MyMoneySplit(); MyMoneySplit otherSplit; foreach (const auto split, t.splits()) { MyMoneySplit s(split); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); s.clearId(); s.setBankID(QString()); // older versions of KMyMoney used to set the action // we don't need this anymore if (s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization) && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(QString()); } // FIXME update check number. The old comment contained // // // If a check number is already specified by the user it is // used. If the input field is empty and the previous transaction // contains a checknumber, the next usable check number will be assigned // to the transaction. // auto editNr = dynamic_cast(haveWidget("number")); if (editNr && !editNr->text().isEmpty()) { s.setNumber(editNr->text()); } else if (!s.number().isEmpty()) { s.setNumber(KMyMoneyUtils::nextFreeCheckNumber(d->m_account)); } // if the memos should not be used with autofill or // if the transaction has exactly two splits, remove // the memo text of the split that does not reference // the current account. This allows the user to change // the autofilled memo text which will then also be used // in this split. See createTransaction() for this logic. if ((s.accountId() != d->m_account.id() && t.splitCount() == 2) || !KMyMoneySettings::autoFillUseMemos()) s.setMemo(QString()); d->m_transaction.addSplit(s); if (s.accountId() == d->m_account.id() && d->m_split == MyMoneySplit()) { d->m_split = s; } else { otherSplit = s; } } // make sure to extract the right action eRegister::Action action; action = d->m_split.shares().isNegative() ? eRegister::Action::Withdrawal : eRegister::Action::Deposit; if (d->m_transaction.splitCount() == 2) { auto acc = MyMoneyFile::instance()->account(otherSplit.accountId()); if (acc.isAssetLiability()) action = eRegister::Action::Transfer; } // now setup the widgets with the new data but keep the date if (auto postdateWidget = dynamic_cast(d->m_editWidgets["postdate"])) { auto date = postdateWidget->date(); loadEditWidgets(action); postdateWidget->setDate(date); } } } // focus jumps into the category field QWidget* w; if ((w = haveWidget("payee")) != 0) { w->setFocus(); } } void StdTransactionEditor::slotUpdateAction(int action) { Q_D(StdTransactionEditor); auto tabbar = dynamic_cast(haveWidget("tabbar")); if (tabbar) { auto categoryLabel = dynamic_cast(haveWidget("category-label")); auto cashflow = dynamic_cast(d->m_editWidgets["cashflow"]); if (!categoryLabel || !cashflow) return; switch (action) { case (int)eRegister::Action::Deposit: categoryLabel->setText(i18n("Category")); cashflow->setDirection(eRegister::CashFlowDirection::Deposit); break; case (int)eRegister::Action::Transfer: if (d->m_split.shares().isNegative()) { cashflow->setDirection(eRegister::CashFlowDirection::Payment); categoryLabel->setText(i18n("Transfer to")); } else { cashflow->setDirection(eRegister::CashFlowDirection::Deposit); categoryLabel->setText(i18n("Transfer from")); } tabbar->setCurrentIndex((int)eRegister::Action::Transfer); slotUpdateCashFlow(cashflow->direction()); break; case (int)eRegister::Action::Withdrawal: categoryLabel->setText(i18n("Category")); cashflow->setDirection(eRegister::CashFlowDirection::Payment); break; } resizeForm(); } } void StdTransactionEditor::slotUpdateCashFlow(eRegister::CashFlowDirection dir) { auto categoryLabel = dynamic_cast(haveWidget("category-label")); if (auto cashflow = dynamic_cast(haveWidget("cashflow"))) cashflow->setDirection(dir); // qDebug("Update cashflow to %d", dir); if (categoryLabel) { auto tabbar = dynamic_cast(haveWidget("tabbar")); if (!tabbar) return; // no transaction form if (categoryLabel->text() != i18n("Category")) { tabbar->setCurrentIndex((int)eRegister::Action::Transfer); if (dir == eRegister::CashFlowDirection::Deposit) { categoryLabel->setText(i18n("Transfer from")); } else { categoryLabel->setText(i18n("Transfer to")); } resizeForm(); } else { if (dir == eRegister::CashFlowDirection::Deposit) tabbar->setCurrentIndex((int)eRegister::Action::Deposit); else tabbar->setCurrentIndex((int)eRegister::Action::Withdrawal); } } } void StdTransactionEditor::slotUpdateCategory(const QString& id) { Q_D(StdTransactionEditor); auto categoryLabel = dynamic_cast(haveWidget("category-label")); // qDebug("Update category to %s", qPrintable(id)); if (categoryLabel) { auto tabbar = dynamic_cast(haveWidget("tabbar")); auto amount = dynamic_cast(d->m_editWidgets["amount"]); auto val = amount ? amount->value() : MyMoneyMoney(); if (categoryLabel->text() == i18n("Transfer from")) { val = -val; } else { val = val.abs(); } if (tabbar) { tabbar->setTabEnabled((int)eRegister::Action::Transfer, true); tabbar->setTabEnabled((int)eRegister::Action::Deposit, true); tabbar->setTabEnabled((int)eRegister::Action::Withdrawal, true); } bool disableTransferTab = false; if (!id.isEmpty()) { auto acc = MyMoneyFile::instance()->account(id); if (acc.isAssetLiability() || acc.accountGroup() == eMyMoney::Account::Type::Equity) { if (tabbar) { tabbar->setCurrentIndex((int)eRegister::Action::Transfer); tabbar->setTabEnabled((int)eRegister::Action::Deposit, false); tabbar->setTabEnabled((int)eRegister::Action::Withdrawal, false); } auto cashflow = dynamic_cast(d->m_editWidgets["cashflow"]); if (val.isZero()) { if (cashflow && (cashflow->direction() == eRegister::CashFlowDirection::Deposit)) { categoryLabel->setText(i18n("Transfer from")); } else { categoryLabel->setText(i18n("Transfer to")); } } else if (val.isNegative()) { categoryLabel->setText(i18n("Transfer from")); if (cashflow) cashflow->setDirection(eRegister::CashFlowDirection::Deposit); } else categoryLabel->setText(i18n("Transfer to")); } else { disableTransferTab = true; categoryLabel->setText(i18n("Category")); } updateAmount(val); } else { //id.isEmpty() if (auto category = dynamic_cast(d->m_editWidgets["category"])) disableTransferTab = !category->currentText().isEmpty(); categoryLabel->setText(i18n("Category")); } if (tabbar) { if (disableTransferTab) { // set the proper tab before disabling the currently active tab if (tabbar->currentIndex() == (int)eRegister::Action::Transfer) { tabbar->setCurrentIndex(val.isPositive() ? (int)eRegister::Action::Withdrawal : (int)eRegister::Action::Deposit); } tabbar->setTabEnabled((int)eRegister::Action::Transfer, false); } tabbar->update(); } resizeForm(); } updateVAT(false); } void StdTransactionEditor::slotUpdatePayment(const QString& txt) { Q_D(StdTransactionEditor); MyMoneyMoney val(txt); auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"]); auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"]); if (!depositWidget || !paymentWidget) return; if (val.isNegative()) { depositWidget->setValue(val.abs()); paymentWidget->clearText(); } else { depositWidget->clearText(); } updateVAT(); } void StdTransactionEditor::slotUpdateDeposit(const QString& txt) { Q_D(StdTransactionEditor); MyMoneyMoney val(txt); auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"]); auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"]); if (!depositWidget || !paymentWidget) return; if (val.isNegative()) { paymentWidget->setValue(val.abs()); depositWidget->clearText(); } else { paymentWidget->clearText(); } updateVAT(); } void StdTransactionEditor::slotUpdateAmount(const QString& txt) { // qDebug("Update amount to %s", qPrintable(txt)); MyMoneyMoney val(txt); updateAmount(val); updateVAT(true); } void StdTransactionEditor::updateAmount(const MyMoneyMoney& val) { // we don't do anything if we have multiple transactions selected if (isMultiSelection()) return; Q_D(StdTransactionEditor); auto categoryLabel = dynamic_cast(haveWidget("category-label")); if (categoryLabel) { if (auto cashflow = dynamic_cast(d->m_editWidgets["cashflow"])) { if (!val.isPositive()) { // fixes BUG321317 if (categoryLabel->text() != i18n("Category")) { if (cashflow->direction() == eRegister::CashFlowDirection::Payment) { categoryLabel->setText(i18n("Transfer to")); } } else { slotUpdateCashFlow(cashflow->direction()); } if (auto amountWidget = dynamic_cast(d->m_editWidgets["amount"])) amountWidget->setValue(val.abs()); } else { if (categoryLabel->text() != i18n("Category")) { if (cashflow->direction() == eRegister::CashFlowDirection::Payment) { categoryLabel->setText(i18n("Transfer to")); } else { categoryLabel->setText(i18n("Transfer from")); cashflow->setDirection(eRegister::CashFlowDirection::Deposit); // editing with +ve shows 'from' not 'pay to' } } if (auto amountWidget = dynamic_cast(d->m_editWidgets["amount"])) amountWidget->setValue(val.abs()); } } } } void StdTransactionEditor::updateVAT(bool amountChanged) { Q_D(StdTransactionEditor); // make sure that we don't do this recursively if (d->m_inUpdateVat) return; // we don't do anything if we have multiple transactions selected if (isMultiSelection()) return; // if auto vat assignment for this account is turned off // we don't care about taxes if (d->m_account.value("NoVat") == "Yes") return; // more splits than category and tax are not supported if (d->m_splits.count() > 2) return; // in order to do anything, we need an amount MyMoneyMoney amount, newAmount; bool amountOk; amount = amountFromWidget(&amountOk); if (!amountOk) return; // If the transaction has a tax and a category split, remove the tax split if (d->m_splits.count() == 2) { newAmount = removeVatSplit(); if (d->m_splits.count() == 2) // not removed? return; } else if (auto category = dynamic_cast(d->m_editWidgets["category"])) { // otherwise, we need a category if (category->selectedItem().isEmpty()) return; // if no VAT account is associated with this category/account, then we bail out MyMoneyAccount cat = MyMoneyFile::instance()->account(category->selectedItem()); if (cat.value("VatAccount").isEmpty()) return; newAmount = amount; } // seems we have everything we need if (amountChanged) newAmount = amount; MyMoneyTransaction transaction; if (createTransaction(transaction, d->m_transaction, d->m_split)) { if (addVatSplit(transaction, newAmount)) { d->m_transaction = transaction; if (!d->m_transaction.splits().isEmpty()) d->m_split = d->m_transaction.splits().front(); loadEditWidgets(); // if we made this a split transaction, then move the // focus to the memo field if (qApp->focusWidget() == haveWidget("category")) { QWidget* w = haveWidget("memo"); if (w) w->setFocus(); } } } } bool StdTransactionEditor::addVatSplit(MyMoneyTransaction& tr, const MyMoneyMoney& amount) { if (tr.splitCount() != 2) return false; Q_D(StdTransactionEditor); auto file = MyMoneyFile::instance(); // extract the category split from the transaction MyMoneyAccount category = file->account(tr.splitByAccount(d->m_account.id(), false).accountId()); return file->addVATSplit(tr, d->m_account, category, amount); } MyMoneyMoney StdTransactionEditor::removeVatSplit() { Q_D(StdTransactionEditor); // we only deal with splits that have three splits if (d->m_splits.count() != 2) return amountFromWidget(); MyMoneySplit c; // category split MyMoneySplit t; // tax split auto netValue = false; foreach (const auto split , d->m_splits) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.value("VatAccount").isEmpty()) { netValue = (acc.value("VatAmount").toLower() == "net"); c = split; } else if (!acc.value("VatRate").isEmpty()) { t = split; } } // bail out if not all splits are setup if (c.id().isEmpty() || t.id().isEmpty()) return amountFromWidget(); MyMoneyMoney amount; // reduce the splits if (netValue) { amount = -c.shares(); } else { amount = -(c.shares() + t.shares()); } // remove tax split from the list, ... d->m_splits.clear(); d->m_splits.append(c); // ... make sure that the widget is updated ... // block the signals to avoid popping up the split editor dialog // for nothing d->m_editWidgets["category"]->blockSignals(true); QString id; setupCategoryWidget(id); d->m_editWidgets["category"]->blockSignals(false); // ... and return the updated amount return amount; } bool StdTransactionEditor::isComplete(QString& reason) const { Q_D(const StdTransactionEditor); reason.clear(); QMap::const_iterator it_w; auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate) { QDate accountOpeningDate = d->m_account.openingDate(); for (QList::const_iterator it_s = d->m_splits.constBegin(); it_s != d->m_splits.constEnd(); ++it_s) { const MyMoneyAccount& acc = MyMoneyFile::instance()->account((*it_s).accountId()); // compute the newest opening date of all accounts involved in the transaction if (acc.openingDate() > accountOpeningDate) accountOpeningDate = acc.openingDate(); } // check the selected category in case m_splits hasn't been updated yet auto category = dynamic_cast(d->m_editWidgets["category"]); if (category && !category->selectedItem().isEmpty()) { MyMoneyAccount cat = MyMoneyFile::instance()->account(category->selectedItem()); if (cat.openingDate() > accountOpeningDate) accountOpeningDate = cat.openingDate(); } if (postDate->date().isValid() && (postDate->date() < accountOpeningDate)) { postDate->markAsBadDate(true, KMyMoneySettings::schemeColor(SchemeColor::Negative)); reason = i18n("Cannot enter transaction with postdate prior to account's opening date."); postDate->setToolTip(reason); return false; } postDate->markAsBadDate(); - postDate->setToolTip(""); + postDate->setToolTip(QString()); } for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { auto payee = dynamic_cast(*it_w); auto tagContainer = dynamic_cast(*it_w); auto category = dynamic_cast(*it_w); auto amount = dynamic_cast(*it_w); auto reconcile = dynamic_cast(*it_w); auto cashflow = dynamic_cast(*it_w); auto memo = dynamic_cast(*it_w); if (payee && !(payee->currentText().isEmpty())) break; if (category && !category->lineEdit()->text().isEmpty()) break; if (amount && !(amount->value().isZero())) break; // the following widgets are only checked if we are editing multiple transactions if (isMultiSelection()) { if (auto tabbar = dynamic_cast(haveWidget("tabbar"))) tabbar->setEnabled(true); if (reconcile && reconcile->state() != eMyMoney::Split::State::Unknown) break; if (cashflow && cashflow->direction() != eRegister::CashFlowDirection::Unknown) break; if (postDate && postDate->date().isValid() && (postDate->date() >= d->m_account.openingDate())) break; if (memo && d->m_memoChanged) break; if (tagContainer && !(tagContainer->selectedTags().isEmpty())) // Tag is optional field break; } } return it_w != d->m_editWidgets.end(); } void StdTransactionEditor::slotCreateCategory(const QString& name, QString& id) { Q_D(StdTransactionEditor); MyMoneyAccount acc, parent; acc.setName(name); auto cashflow = dynamic_cast(haveWidget("cashflow")); if (cashflow) { // form based input if (cashflow->direction() == eRegister::CashFlowDirection::Deposit) parent = MyMoneyFile::instance()->income(); else parent = MyMoneyFile::instance()->expense(); } else if (haveWidget("deposit")) { // register based input if (auto deposit = dynamic_cast(d->m_editWidgets["deposit"])) { if (deposit->value().isPositive()) parent = MyMoneyFile::instance()->income(); else parent = MyMoneyFile::instance()->expense(); } } else parent = MyMoneyFile::instance()->expense(); // TODO extract possible first part of a hierarchy and check if it is one // of our top categories. If so, remove it and select the parent // according to this information. slotNewCategory(acc, parent); // return id id = acc.id(); } int StdTransactionEditor::slotEditSplits() { Q_D(StdTransactionEditor); int rc = QDialog::Rejected; if (!d->m_openEditSplits) { // only get in here in a single instance d->m_openEditSplits = true; // force focus change to update all data auto categoryWidget = dynamic_cast(d->m_editWidgets["category"]); QWidget* w = categoryWidget ? categoryWidget->splitButton() : nullptr; if (w) w->setFocus(); auto amount = dynamic_cast(haveWidget("amount")); auto deposit = dynamic_cast(haveWidget("deposit")); auto payment = dynamic_cast(haveWidget("payment")); KMyMoneyCashFlowCombo* cashflow = 0; eRegister::CashFlowDirection dir = eRegister::CashFlowDirection::Unknown; bool isValidAmount = false; if (amount) { isValidAmount = amount->lineedit()->text().length() != 0; if ((cashflow = dynamic_cast(haveWidget("cashflow")))) dir = cashflow->direction(); } else { if (deposit) { if (deposit->lineedit()->text().length() != 0) { isValidAmount = true; dir = eRegister::CashFlowDirection::Deposit; } } if (payment) { if (payment->lineedit()->text().length() != 0) { isValidAmount = true; dir = eRegister::CashFlowDirection::Payment; } } if (!deposit || !payment) { qDebug("Internal error: deposit(%p) & payment(%p) widgets not found but required", deposit, payment); return rc; } } if (dir == eRegister::CashFlowDirection::Unknown) dir = eRegister::CashFlowDirection::Payment; MyMoneyTransaction transaction; if (createTransaction(transaction, d->m_transaction, d->m_split)) { MyMoneyMoney value; QPointer dlg = new KSplitTransactionDlg(transaction, transaction.splits().isEmpty() ? MyMoneySplit() : transaction.splits().front(), d->m_account, isValidAmount, dir == eRegister::CashFlowDirection::Deposit, MyMoneyMoney(), d->m_priceInfo, d->m_regForm); connect(dlg.data(), &KSplitTransactionDlg::objectCreation, this, &StdTransactionEditor::objectCreation); connect(dlg.data(), &KSplitTransactionDlg::createCategory, this, &StdTransactionEditor::slotNewCategory); if ((rc = dlg->exec()) == QDialog::Accepted) { d->m_transaction = dlg->transaction(); if (!d->m_transaction.splits().isEmpty()) d->m_split = d->m_transaction.splits().front(); loadEditWidgets(); } delete dlg; } // focus jumps into the tag field if ((w = haveWidget("tag")) != 0) { w->setFocus(); } d->m_openEditSplits = false; } return rc; } void StdTransactionEditor::checkPayeeInSplit(MyMoneySplit& s, const QString& payeeId) { if (s.accountId().isEmpty()) return; auto acc = MyMoneyFile::instance()->account(s.accountId()); if (acc.isIncomeExpense()) { s.setPayeeId(payeeId); } else { if (s.payeeId().isEmpty()) s.setPayeeId(payeeId); } } MyMoneyMoney StdTransactionEditor::amountFromWidget(bool* update) const { Q_D(const StdTransactionEditor); bool updateValue = false; MyMoneyMoney value; auto cashflow = dynamic_cast(haveWidget("cashflow")); if (cashflow) { // form based input if (auto amount = dynamic_cast(d->m_editWidgets["amount"])) { // if both fields do not contain changes -> no need to update if (cashflow->direction() != eRegister::CashFlowDirection::Unknown && !amount->lineedit()->text().isEmpty()) updateValue = true; value = amount->value(); if (cashflow->direction() == eRegister::CashFlowDirection::Payment) value = -value; } } else if (haveWidget("deposit")) { // register based input auto deposit = dynamic_cast(d->m_editWidgets["deposit"]); auto payment = dynamic_cast(d->m_editWidgets["payment"]); if (deposit && payment) { // if both fields do not contain text -> no need to update if (!(deposit->lineedit()->text().isEmpty() && payment->lineedit()->text().isEmpty())) updateValue = true; if (deposit->value().isPositive()) value = deposit->value(); else value = -(payment->value()); } } if (update) *update = updateValue; // determine the max fraction for this account and // adjust the value accordingly return value.convert(d->m_account.fraction()); } bool StdTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyMoneyTransaction& torig, const MyMoneySplit& sorig, bool skipPriceDialog) { Q_D(StdTransactionEditor); // extract price info from original transaction d->m_priceInfo.clear(); if (!torig.id().isEmpty()) { foreach (const auto split, torig.splits()) { if (split.id() != sorig.id()) { MyMoneyAccount cat = MyMoneyFile::instance()->account(split.accountId()); if (cat.currencyId() != d->m_account.currencyId()) { if (!split.shares().isZero() && !split.value().isZero()) { d->m_priceInfo[cat.currencyId()] = (split.shares() / split.value()).reduce(); } } } } } t = torig; t.removeSplits(); t.setCommodity(d->m_account.currencyId()); auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate && postDate->date().isValid()) { t.setPostDate(postDate->date()); } // we start with the previous values, make sure we can add them later on MyMoneySplit s0 = sorig; s0.clearId(); // make sure we reference this account here s0.setAccountId(d->m_account.id()); // memo and number field are special: if we have multiple transactions selected // and the edit field is empty, we treat it as "not modified". // FIXME a better approach would be to have a 'dirty' flag with the widgets // which identifies if the originally loaded value has been modified // by the user auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { if (!isMultiSelection() || d->m_memoChanged) s0.setMemo(memo->toPlainText()); } if (auto number = dynamic_cast(haveWidget("number"))) { if (!isMultiSelection() || !number->text().isEmpty()) s0.setNumber(number->text()); } auto payee = dynamic_cast(d->m_editWidgets["payee"]); QString payeeId; if (payee && (!isMultiSelection() || !payee->currentText().isEmpty())) { payeeId = payee->selectedItem(); s0.setPayeeId(payeeId); } //KMyMoneyTagCombo* tag = dynamic_cast(m_editWidgets["tag"]); auto tag = dynamic_cast(d->m_editWidgets["tag"]); if (tag && (!isMultiSelection() || !tag->selectedTags().isEmpty())) { s0.setTagIdList(tag->selectedTags()); } bool updateValue; MyMoneyMoney value = amountFromWidget(&updateValue); if (updateValue) { // for this account, the shares and value is the same s0.setValue(value); s0.setShares(value); } else { value = s0.value(); } // if we mark the split reconciled here, we'll use today's date if no reconciliation date is given auto status = dynamic_cast(d->m_editWidgets["status"]); if (status && status->state() != eMyMoney::Split::State::Unknown) s0.setReconcileFlag(status->state()); if (s0.reconcileFlag() == eMyMoney::Split::State::Reconciled && !s0.reconcileDate().isValid()) s0.setReconcileDate(QDate::currentDate()); checkPayeeInSplit(s0, payeeId); // add the split to the transaction t.addSplit(s0); // if we have no other split we create it // if we have none or only one other split, we reconstruct it here // if we have more than one other split, we take them as they are // make sure to perform all those changes on a local copy QList splits = d->m_splits; MyMoneySplit s1; if (splits.isEmpty()) { s1.setMemo(s0.memo()); splits.append(s1); // make sure we will fill the value and share fields later on updateValue = true; } // FIXME in multiSelection we currently only support transactions with one // or two splits. So we check the original transaction and extract the other // split or create it if (isMultiSelection()) { if (torig.splitCount() == 2) { foreach (const auto split, torig.splits()) { if (split.id() == sorig.id()) continue; s1 = split; s1.clearId(); break; } } } else { if (splits.count() == 1) { s1 = splits[0]; s1.clearId(); } } if (isMultiSelection() || splits.count() == 1) { auto category = dynamic_cast(d->m_editWidgets["category"]); if (category && (!isMultiSelection() || !category->currentText().isEmpty())) { s1.setAccountId(category->selectedItem()); } // if the first split has a memo but the second split is empty, // we just copy the memo text over if (memo) { if (!isMultiSelection() || !memo->toPlainText().isEmpty()) { // if the memo is filled, we check if the // account referenced by s1 is a regular account or a category. // in case of a regular account, we just leave the memo as is // in case of a category we simply copy the new value over the old. // in case we don't even have an account id, we just skip because // the split will be removed later on anyway. if (!s1.memo().isEmpty() && s1.memo() != s0.memo()) { if (!s1.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(s1.accountId()); if (acc.isIncomeExpense()) s1.setMemo(s0.memo()); else if (KMessageBox::questionYesNo(d->m_regForm, i18n("Do you want to replace memo

%1

with memo

%2

in the other split?", s1.memo(), s0.memo()), i18n("Copy memo"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("CopyMemoOver")) == KMessageBox::Yes) s1.setMemo(s0.memo()); } } else { s1.setMemo(s0.memo()); } } } if (updateValue && !s1.accountId().isEmpty()) { s1.setValue(-value); MyMoneyMoney shares; if (!skipPriceDialog) { if (!KCurrencyCalculator::setupSplitPrice(shares, t, s1, d->m_priceInfo, d->m_regForm)) return false; } else { MyMoneyAccount cat = MyMoneyFile::instance()->account(s1.accountId()); if (d->m_priceInfo.find(cat.currencyId()) != d->m_priceInfo.end()) { shares = (s1.value() * d->m_priceInfo[cat.currencyId()]).reduce().convert(cat.fraction()); } else shares = s1.value(); } s1.setShares(shares); } checkPayeeInSplit(s1, payeeId); if (!s1.accountId().isEmpty()) t.addSplit(s1); // check if we need to add/update a VAT assignment MyMoneyFile::instance()->updateVAT(t); } else { foreach (const auto split, splits) { s1 = split; s1.clearId(); checkPayeeInSplit(s1, payeeId); t.addSplit(s1); } } return true; } void StdTransactionEditor::setupFinalWidgets() { addFinalWidget(haveWidget("deposit")); addFinalWidget(haveWidget("payment")); addFinalWidget(haveWidget("amount")); addFinalWidget(haveWidget("status")); } void StdTransactionEditor::slotUpdateAccount(const QString& id) { Q_D(StdTransactionEditor); TransactionEditor::slotUpdateAccount(id); auto category = dynamic_cast(d->m_editWidgets["category"]); if (category && category->splitButton()) { category->splitButton()->setDisabled(id.isEmpty()); } } diff --git a/kmymoney/dialogs/transactioneditor.cpp b/kmymoney/dialogs/transactioneditor.cpp index 6628cbb3d..44fcc1edc 100644 --- a/kmymoney/dialogs/transactioneditor.cpp +++ b/kmymoney/dialogs/transactioneditor.cpp @@ -1,846 +1,858 @@ /* * Copyright 2007-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "transactioneditor.h" #include "transactioneditor_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneytagcombo.h" #include "knewinvestmentwizard.h" #include "knewaccountdlg.h" #include "ktagcontainer.h" #include "tabbar.h" #include "mymoneyutils.h" #include "mymoneyexception.h" #include "kmymoneycategory.h" #include "kmymoneymvccombo.h" #include "kmymoneyedit.h" #include "kmymoneylineedit.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "mymoneysecurity.h" #include "kmymoneyutils.h" #include "kmymoneycompletion.h" #include "transaction.h" #include "transactionform.h" #include "kmymoneysettings.h" #include "transactioneditorcontainer.h" #include "kcurrencycalculator.h" #include "icons.h" using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Icons; TransactionEditor::TransactionEditor() : d_ptr(new TransactionEditorPrivate(this)) { Q_D(TransactionEditor); d->init(); } TransactionEditor::TransactionEditor(TransactionEditorPrivate &dd, TransactionEditorContainer* regForm, KMyMoneyRegister::Transaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : d_ptr(&dd) // d_ptr(new TransactionEditorPrivate) { Q_D(TransactionEditor); d->m_paymentMethod = eMyMoney::Schedule::PaymentType::Any; d->m_transactions = list; d->m_regForm = regForm; d->m_item = item; d->m_transaction = item->transaction(); d->m_split = item->split(); d->m_lastPostDate = lastPostDate; d->m_initialAction = eWidgets::eRegister::Action::None; d->m_openEditSplits = false; d->m_memoChanged = false; d->m_item->startEditMode(); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, static_cast(&TransactionEditor::slotUpdateAccount)); } TransactionEditor::TransactionEditor(TransactionEditorPrivate &dd) : d_ptr(&dd) { Q_D(TransactionEditor); d->init(); } TransactionEditor::~TransactionEditor() { Q_D(TransactionEditor); // Make sure the widgets do not send out signals to the editor anymore // After all, the editor is about to die //disconnect first tagCombo: auto w = dynamic_cast(haveWidget("tag")); if (w && w->tagCombo()) { w->tagCombo()->disconnect(this); } QMap::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->disconnect(this); } d->m_regForm->removeEditWidgets(d->m_editWidgets); d->m_item->leaveEditMode(); emit finishEdit(d->m_transactions); } void TransactionEditor::slotUpdateAccount(const QString& id) { Q_D(TransactionEditor); d->m_account = MyMoneyFile::instance()->account(id); setupPrecision(); } void TransactionEditor::slotUpdateAccount() { Q_D(TransactionEditor); // reload m_account as it might have been changed d->m_account = MyMoneyFile::instance()->account(d->m_account.id()); setupPrecision(); } void TransactionEditor::setupPrecision() { Q_D(TransactionEditor); const int prec = (d->m_account.id().isEmpty()) ? 2 : MyMoneyMoney::denomToPrec(d->m_account.fraction()); QStringList widgets = QString("amount,deposit,payment").split(','); QStringList::const_iterator it_w; for (it_w = widgets.constBegin(); it_w != widgets.constEnd(); ++it_w) { QWidget * w; if ((w = haveWidget(*it_w)) != 0) { if (auto precisionWidget = dynamic_cast(w)) precisionWidget->setPrecision(prec); } } } void TransactionEditor::setup(QWidgetList& tabOrderWidgets, const MyMoneyAccount& account, eWidgets::eRegister::Action action) { Q_D(TransactionEditor); d->m_account = account; d->m_initialAction = action; createEditWidgets(); d->m_regForm->arrangeEditWidgets(d->m_editWidgets, d->m_item); d->m_regForm->tabOrder(tabOrderWidgets, d->m_item); QWidget* w = haveWidget("tabbar"); if (w) { tabOrderWidgets.append(w); auto tabbar = dynamic_cast(w); if ((tabbar) && (action == eWidgets::eRegister::Action::None)) { action = static_cast(tabbar->currentIndex()); } } loadEditWidgets(action); // remove all unused widgets and don't forget to remove them // from the tab order list as well d->m_editWidgets.removeOrphans(); QWidgetList::iterator it_w; const QWidgetList editWidgets(d->m_editWidgets.values()); for (it_w = tabOrderWidgets.begin(); it_w != tabOrderWidgets.end();) { if (editWidgets.contains(*it_w)) { ++it_w; } else { // before we remove the widget, we make sure it's not a part of a known one. // these could be a direct child in case of KMyMoneyDateInput and KMyMoneyEdit // where we store the pointer to the surrounding frame in editWidgets // or the parent is called "KMyMoneyCategoryFrame" if (*it_w) { if (editWidgets.contains((*it_w)->parentWidget()) || ((*it_w)->parentWidget() && (*it_w)->parentWidget()->objectName() == QLatin1String("KMyMoneyCategoryFrame"))) { ++it_w; } else { // qDebug("Remove '%s' from taborder", qPrintable((*it_w)->objectName())); it_w = tabOrderWidgets.erase(it_w); } } else { it_w = tabOrderWidgets.erase(it_w); } } } clearFinalWidgets(); setupFinalWidgets(); slotUpdateButtonState(); } void TransactionEditor::setup(QWidgetList& tabOrderWidgets, const MyMoneyAccount& account) { setup(tabOrderWidgets, account, eWidgets::eRegister::Action::None); } MyMoneyAccount TransactionEditor::account() const { Q_D(const TransactionEditor); return d->m_account; } void TransactionEditor::setScheduleInfo(const QString& si) { Q_D(TransactionEditor); d->m_scheduleInfo = si; } void TransactionEditor::setPaymentMethod(eMyMoney::Schedule::PaymentType pm) { Q_D(TransactionEditor); d->m_paymentMethod = pm; } void TransactionEditor::clearFinalWidgets() { Q_D(TransactionEditor); d->m_finalEditWidgets.clear(); } void TransactionEditor::addFinalWidget(const QWidget* w) { Q_D(TransactionEditor); if (w) { d->m_finalEditWidgets << w; } } void TransactionEditor::slotReloadEditWidgets() { } bool TransactionEditor::eventFilter(QObject* o, QEvent* e) { Q_D(TransactionEditor); bool rc = false; if (o == haveWidget("number")) { if (e->type() == QEvent::MouseButtonDblClick) { assignNextNumber(); rc = true; } } // if the object is a widget, the event is a key press event and // the object is one of our edit widgets, then .... auto numberWiget = dynamic_cast(o); if (o->isWidgetType() && (e->type() == QEvent::KeyPress) && numberWiget && d->m_editWidgets.values().contains(numberWiget)) { auto k = dynamic_cast(e); if ((k && (k->modifiers() & Qt::KeyboardModifierMask)) == 0 || (k && (k->modifiers() & Qt::KeypadModifier)) != 0) { bool isFinal = false; QList::const_iterator it_w; switch (k->key()) { case Qt::Key_Return: case Qt::Key_Enter: // we check, if the object is one of the m_finalEditWidgets and if it's // a KMyMoneyEdit object that the value is not 0. If any of that is the // case, it's the final object. In other cases, we convert the enter // key into a TAB key to move between the fields. Of course, we only need // to do this as long as the appropriate option is set. In all other cases, // we treat the return/enter key as such. if (KMyMoneySettings::enterMovesBetweenFields()) { for (it_w = d->m_finalEditWidgets.constBegin(); !isFinal && it_w != d->m_finalEditWidgets.constEnd(); ++it_w) { if (*it_w == o) { if (auto widget = dynamic_cast(*it_w)) { isFinal = !(widget->value().isZero()); } else isFinal = true; } } } else isFinal = true; // for the non-final objects, we treat the return key as a TAB if (!isFinal) { QKeyEvent evt(e->type(), Qt::Key_Tab, k->modifiers(), QString(), k->isAutoRepeat(), k->count()); QApplication::sendEvent(o, &evt); // in case of a category item and the split button is visible // send a second event so that we get passed the button. auto widget = dynamic_cast(o); if (widget && widget->splitButton()) QApplication::sendEvent(o, &evt); } else { QTimer::singleShot(0, this, SIGNAL(returnPressed())); } // don't process any further rc = true; break; case Qt::Key_Escape: QTimer::singleShot(0, this, SIGNAL(escapePressed())); break; } } } return rc; } void TransactionEditor::slotUpdateMemoState() { Q_D(TransactionEditor); KTextEdit* memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_memoChanged = (memo->toPlainText() != d->m_memoText); } } void TransactionEditor::slotUpdateButtonState() { QString reason; emit transactionDataSufficient(isComplete(reason)); } QWidget* TransactionEditor::haveWidget(const QString& name) const { Q_D(const TransactionEditor); return d->m_editWidgets.haveWidget(name); } int TransactionEditor::slotEditSplits() { return QDialog::Rejected; } void TransactionEditor::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s) { Q_D(TransactionEditor); d->m_transaction = t; d->m_split = s; loadEditWidgets(); } bool TransactionEditor::isMultiSelection() const { Q_D(const TransactionEditor); return d->m_transactions.count() > 1; } bool TransactionEditor::fixTransactionCommodity(const MyMoneyAccount& account) { Q_D(TransactionEditor); bool rc = true; bool firstTimeMultiCurrency = true; d->m_account = account; auto file = MyMoneyFile::instance(); // determine the max fraction for this account MyMoneySecurity sec = file->security(d->m_account.currencyId()); int fract = d->m_account.fraction(); // scan the list of selected transactions KMyMoneyRegister::SelectedTransactions::iterator it_t; for (it_t = d->m_transactions.begin(); (rc == true) && (it_t != d->m_transactions.end()); ++it_t) { // there was a time when the schedule editor did not setup the transaction commodity // let's give a helping hand here for those old schedules if ((*it_t).transaction().commodity().isEmpty()) (*it_t).transaction().setCommodity(d->m_account.currencyId()); // we need to check things only if a different commodity is used if (d->m_account.currencyId() != (*it_t).transaction().commodity()) { MyMoneySecurity osec = file->security((*it_t).transaction().commodity()); switch ((*it_t).transaction().splitCount()) { case 0: // new transaction, guess nothing's here yet ;) break; case 1: try { // make sure, that the value is equal to the shares, don't forget our own copy MyMoneySplit& splitB = (*it_t).split(); // reference usage wanted here if (d->m_split == splitB) d->m_split.setValue(splitB.shares()); splitB.setValue(splitB.shares()); (*it_t).transaction().modifySplit(splitB); } catch (const MyMoneyException &e) { qDebug("Unable to update commodity to second splits currency in %s: '%s'", qPrintable((*it_t).transaction().id()), e.what()); } break; case 2: // If we deal with multiple currencies we make sure, that for // transactions with two splits, the transaction's commodity is the // currency of the currently selected account. This saves us from a // lot of grieve later on. We just have to switch the // transactions commodity. Let's assume the following scenario: // - transactions commodity is CA // - splitB and account's currencyId is CB // - splitA is of course in CA (otherwise we have a real problem) // - Value is V in both splits // - Shares in splitB is SB // - Shares in splitA is SA (and equal to V) // // We do the following: // - change transactions commodity to CB // - set V in both splits to SB // - modify the splits in the transaction try { // retrieve the splits MyMoneySplit& splitB = (*it_t).split(); // reference usage wanted here MyMoneySplit splitA = (*it_t).transaction().splitByAccount(d->m_account.id(), false); // - set V in both splits to SB. Don't forget our own copy if (d->m_split == splitB) { d->m_split.setValue(splitB.shares()); } splitB.setValue(splitB.shares()); splitA.setValue(-splitB.shares()); (*it_t).transaction().modifySplit(splitA); (*it_t).transaction().modifySplit(splitB); } catch (const MyMoneyException &e) { qDebug("Unable to update commodity to second splits currency in %s: '%s'", qPrintable((*it_t).transaction().id()), e.what()); } break; default: // TODO: use new logic by adjusting all splits by the price // extracted from the selected split. Inform the user that // this will happen and allow him to stop the processing (rc = false) try { QString msg; if (firstTimeMultiCurrency) { firstTimeMultiCurrency = false; if (!isMultiSelection()) { msg = i18n("This transaction has more than two splits and is originally based on a different currency (%1). Using this account to modify the transaction may result in rounding errors. Do you want to continue?", osec.name()); } else { msg = i18n("At least one of the selected transactions has more than two splits and is originally based on a different currency (%1). Using this account to modify the transactions may result in rounding errors. Do you want to continue?", osec.name()); } if (KMessageBox::warningContinueCancel(0, QString("%1").arg(msg)) == KMessageBox::Cancel) { rc = false; } } if (rc == true) { MyMoneyMoney price; if (!(*it_t).split().shares().isZero() && !(*it_t).split().value().isZero()) price = (*it_t).split().shares() / (*it_t).split().value(); MyMoneySplit& mySplit = (*it_t).split(); foreach (const auto split, (*it_t).transaction().splits()) { auto s = split; if (s == mySplit) { s.setValue(s.shares()); if (mySplit == d->m_split) { d->m_split = s; } mySplit = s; } else { s.setValue((s.value() * price).convert(fract)); } (*it_t).transaction().modifySplit(s); } } } catch (const MyMoneyException &e) { qDebug("Unable to update commodity of split currency in %s: '%s'", qPrintable((*it_t).transaction().id()), e.what()); } break; } // set the transaction's ommodity to this account's currency (*it_t).transaction().setCommodity(d->m_account.currencyId()); // update our copy of the transaction that has the focus if ((*it_t).transaction().id() == d->m_transaction.id()) { d->m_transaction = (*it_t).transaction(); } } } return rc; } QString TransactionEditor::validateCheckNumber(const QString& num) const { Q_D(const TransactionEditor); int rc = KMessageBox::No; QString schedInfo; if (!d->m_scheduleInfo.isEmpty()) { schedInfo = i18n("
Processing schedule for %1.
", d->m_scheduleInfo); } if (MyMoneyFile::instance()->checkNoUsed(d->m_account.id(), num)) { rc = KMessageBox::questionYesNo(d->m_regForm, QString("") + schedInfo + i18n("Check number %1 has already been used in account %2." "
Do you want to replace it with the next available number?
", num, d->m_account.name()) + QString("
"), i18n("Duplicate number")); if (rc == KMessageBox::Yes) { return KMyMoneyUtils::nextFreeCheckNumber(d->m_account); } } return num; } void TransactionEditor::assignNextNumber() { Q_D(TransactionEditor); auto number = dynamic_cast(haveWidget("number")); if (number) { const auto num = validateCheckNumber(KMyMoneyUtils::nextCheckNumber(d->m_account)); d->m_account.setValue("lastNumberUsed", num); number->setText(num); } } void TransactionEditor::slotNumberChanged(const QString& txt) { Q_D(TransactionEditor); auto number = dynamic_cast(haveWidget("number")); if (number) { const auto next = validateCheckNumber(txt); if (next != txt) { number->setText(next); } } } bool TransactionEditor::canAssignNumber() const { if (dynamic_cast(haveWidget("number"))) return true; return false; } void TransactionEditor::setupCategoryWidget(KMyMoneyCategory* category, const QList& splits, QString& categoryId, const char* splitEditSlot, bool /* allowObjectCreation */) { disconnect(category, SIGNAL(focusIn()), this, splitEditSlot); #if 0 // FIXME must deal with the logic that suppressObjectCreation is // automatically turned off when the createItem() signal is connected if (allowObjectCreation) category->setSuppressObjectCreation(false); #endif switch (splits.count()) { case 0: categoryId.clear(); if (!category->currentText().isEmpty()) { // category->clearEditText(); // don't clear as could be from another widget - Bug 322768 // make sure, we don't see the selector category->completion()->hide(); } category->completion()->setSelected(QString()); break; case 1: categoryId = splits[0].accountId(); category->completion()->setSelected(categoryId); category->slotItemSelected(categoryId); break; default: categoryId.clear(); category->setSplitTransaction(); connect(category, SIGNAL(focusIn()), this, splitEditSlot); #if 0 // FIXME must deal with the logic that suppressObjectCreation is // automatically turned off when the createItem() signal is connected if (allowObjectCreation) category->setSuppressObjectCreation(true); #endif break; } } bool TransactionEditor::createNewTransaction() const { Q_D(const TransactionEditor); bool rc = true; if (!d->m_transactions.isEmpty()) { rc = d->m_transactions.at(0).transaction().id().isEmpty(); } return rc; } bool TransactionEditor::enterTransactions(QString& newId, bool askForSchedule, bool suppressBalanceWarnings) { Q_D(TransactionEditor); newId.clear(); auto file = MyMoneyFile::instance(); // make sure to run through all stuff that is tied to 'focusout events'. d->m_regForm->parentWidget()->setFocus(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 10); // we don't need to update our widgets anymore, so we just disconnect the signal disconnect(file, &MyMoneyFile::dataChanged, this, &TransactionEditor::slotReloadEditWidgets); KMyMoneyRegister::SelectedTransactions::iterator it_t; MyMoneyTransaction t; bool newTransactionCreated = false; // make sure, that only a single new transaction can be created. // we need to update m_transactions to contain the new transaction // which is then stored in the variable t when we leave the loop. // m_transactions will be sent out in finishEdit() and forces // the new transaction to be selected in the ledger view // collect the transactions to be stored in the engine in a local // list first, so that the user has a chance to interrupt the storage // process QList list; auto storeTransactions = true; // collect transactions for (it_t = d->m_transactions.begin(); storeTransactions && !newTransactionCreated && it_t != d->m_transactions.end(); ++it_t) { storeTransactions = createTransaction(t, (*it_t).transaction(), (*it_t).split()); // if the transaction was created successfully, append it to the list if (storeTransactions) list.append(t); // if we created a new transaction keep that in mind if (t.id().isEmpty()) newTransactionCreated = true; } // if not interrupted by user, continue to store them in the engine if (storeTransactions) { auto i = 0; emit statusMsg(i18n("Storing transactions")); emit statusProgress(0, list.count()); MyMoneyFileTransaction ft; try { QMap minBalanceEarly; QMap minBalanceAbsolute; QMap maxCreditEarly; QMap maxCreditAbsolute; QMap accountIds; for (MyMoneyTransaction& transaction : list) { // if we have a categorization, make sure we remove // the 'imported' flag automagically if (transaction.splitCount() > 1) transaction.setImported(false); // create information about min and max balances foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); accountIds[acc.id()] = true; MyMoneyMoney balance = file->balance(acc.id()); if (!acc.value("minBalanceEarly").isEmpty()) { minBalanceEarly[acc.id()] = balance < MyMoneyMoney(acc.value("minBalanceEarly")); } if (!acc.value("minBalanceAbsolute").isEmpty()) { minBalanceAbsolute[acc.id()] = balance < MyMoneyMoney(acc.value("minBalanceAbsolute")); minBalanceEarly[acc.id()] = false; } if (!acc.value("maxCreditEarly").isEmpty()) { maxCreditEarly[acc.id()] = balance < MyMoneyMoney(acc.value("maxCreditEarly")); } if (!acc.value("maxCreditAbsolute").isEmpty()) { maxCreditAbsolute[acc.id()] = balance < MyMoneyMoney(acc.value("maxCreditAbsolute")); maxCreditEarly[acc.id()] = false; } + + // and adjust opening date of invest accounts + if (acc.isInvest()) { + if (acc.openingDate() > t.postDate()) { + try { + acc.setOpeningDate(t.postDate()); + file->modifyAccount(acc); + } catch(MyMoneyException& ) { + qDebug() << "Unable to modify opening date for invest account" << acc.name() << acc.id(); + } + } + } } if (transaction.id().isEmpty()) { bool enter = true; if (askForSchedule && transaction.postDate() > QDate::currentDate()) { KGuiItem enterButton(i18n("&Enter"), Icons::get(Icon::DialogOK), i18n("Accepts the entered data and stores it"), i18n("Use this to enter the transaction into the ledger.")); KGuiItem scheduleButton(i18n("&Schedule"), Icons::get(Icon::AppointmentNew), i18n("Accepts the entered data and stores it as schedule"), i18n("Use this to schedule the transaction for later entry into the ledger.")); enter = KMessageBox::questionYesNo(d->m_regForm, QString("%1").arg(i18n("The transaction you are about to enter has a post date in the future.

Do you want to enter it in the ledger or add it to the schedules?")), i18nc("Dialog caption for 'Enter or schedule' dialog", "Enter or schedule?"), enterButton, scheduleButton, "EnterOrScheduleTransactionInFuture") == KMessageBox::Yes; } if (enter) { // add new transaction file->addTransaction(transaction); // pass the newly assigned id on to the caller newId = transaction.id(); // refresh account object for transactional changes // refresh account and transaction object because they might have changed d->m_account = file->account(d->m_account.id()); t = transaction; // if a new transaction has a valid number, keep it with the account d->keepNewNumber(transaction); } else { // turn object creation on, so that moving the focus does // not screw up the dialog that might be popping up emit objectCreation(true); emit scheduleTransaction(transaction, eMyMoney::Schedule::Occurrence::Once); emit objectCreation(false); newTransactionCreated = false; } // send out the post date of this transaction emit lastPostDateUsed(transaction.postDate()); } else { // modify existing transaction // its number might have been edited // bearing in mind it could contain alpha characters d->keepNewNumber(transaction); file->modifyTransaction(transaction); } } emit statusProgress(i++, 0); // update m_transactions to contain the newly created transaction so that // it is selected as the current one // we need to do that before we commit the transaction to the engine // as we need it during the update of the views that is caused by committing already. if (newTransactionCreated) { d->m_transactions.clear(); MyMoneySplit s; // a transaction w/o a single split should not exist and adding it // should throw an exception in MyMoneyFile::addTransaction, but we // remain on the save side of things to check for it if (t.splitCount() > 0) s = t.splits().front(); KMyMoneyRegister::SelectedTransaction st(t, s, QString()); d->m_transactions.append(st); } // Save pricing information foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = file->account(id); MyMoneySecurity sec = file->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); file->addPrice(price); break; } ft.commit(); // now analyze the balances and spit out warnings to the user QMap::const_iterator it_a; if (!suppressBalanceWarnings) { for (it_a = accountIds.constBegin(); it_a != accountIds.constEnd(); ++it_a) { QString msg; auto acc = file->account(it_a.key()); MyMoneyMoney balance = file->balance(acc.id()); const MyMoneySecurity& sec = file->security(acc.currencyId()); QString key; key = "minBalanceEarly"; if (!acc.value(key).isEmpty()) { if (minBalanceEarly[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the warning balance of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "minBalanceAbsolute"; if (!acc.value(key).isEmpty()) { if (minBalanceAbsolute[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the minimum balance of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "maxCreditEarly"; if (!acc.value(key).isEmpty()) { if (maxCreditEarly[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the maximum credit warning limit of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "maxCreditAbsolute"; if (!acc.value(key).isEmpty()) { if (maxCreditAbsolute[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the maximum credit limit of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } if (!msg.isEmpty()) { emit balanceWarning(d->m_regForm, acc, msg); } } } } catch (const MyMoneyException &e) { qDebug("Unable to store transaction within engine: %s", e.what()); } emit statusProgress(-1, -1); emit statusMsg(QString()); } return storeTransactions; } void TransactionEditor::resizeForm() { Q_D(TransactionEditor); // force resizeing of the columns in the form if (auto form = dynamic_cast(d->m_regForm)) QMetaObject::invokeMethod(form, "resize", Qt::QueuedConnection, QGenericReturnArgument(), Q_ARG(int, (int)eWidgets::eTransactionForm::Column::Value1)); } void TransactionEditor::slotNewPayee(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void TransactionEditor::slotNewTag(const QString& newnameBase, QString& id) { KMyMoneyUtils::newTag(newnameBase, id); } void TransactionEditor::slotNewCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void TransactionEditor::slotNewInvestment(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index e615fc876..a2b1493f3 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,3737 +1,3764 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart (C) 2017, 2018 by Łukasz Wojniłowicz ****************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #include "kmymoney.h" // ---------------------------------------------------------------------------- // Std C++ / STL Includes #include #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // only for performance tests #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_HOLIDAYS #include #include #endif #ifdef ENABLE_ACTIVITIES #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/knewbankdlg.h" #include "dialogs/ksaveasquestion.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" #include "dialogs/kloadtemplatedlg.h" #include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/kmymoneyaccountselector.h" #include "widgets/kmymoneypayeecombo.h" #include "widgets/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "models/models.h" #include "models/accountsmodel.h" #include "models/equitiesmodel.h" #include "models/securitiesmodel.h" #ifdef ENABLE_UNFINISHEDFEATURES #include "models/ledgermodel.h" #endif #include "mymoney/mymoneyobject.h" #include "mymoney/mymoneyfile.h" #include "mymoney/mymoneyinstitution.h" #include "mymoney/mymoneyaccount.h" #include "mymoney/mymoneyaccountloan.h" #include "mymoney/mymoneysecurity.h" #include "mymoney/mymoneypayee.h" #include "mymoney/mymoneyprice.h" #include "mymoney/mymoneytag.h" #include "mymoney/mymoneybudget.h" #include "mymoney/mymoneyreport.h" #include "mymoney/mymoneysplit.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/mymoneytransactionfilter.h" #include "mymoneyexception.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmappinterface.h" #include "plugins/interfaces/kmmviewinterface.h" #include "plugins/interfaces/kmmstatementinterface.h" #include "plugins/interfaces/kmmimportinterface.h" #include "plugins/interfaceloader.h" #include "plugins/onlinepluginextended.h" #include "pluginloader.h" #include "kmymoneyplugin.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" #include "imymoneystorageformat.h" #include "transactioneditor.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "viewenums.h" #include "menuenums.h" #include "kmymoneyenums.h" #include "misc/platformtools.h" #ifdef ENABLE_SQLCIPHER #include "sqlcipher/sqlite3.h" #endif #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: explicit Private(KMyMoneyApp *app) : q(app), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_myMoneyView(nullptr), m_startDialog(false), m_progressBar(nullptr), m_statusLabel(nullptr), m_autoSaveEnabled(true), m_autoSaveTimer(nullptr), m_progressTimer(nullptr), m_autoSavePeriod(0), m_inAutoSaving(false), m_recentFiles(nullptr), #ifdef ENABLE_HOLIDAYS m_holidayRegion(nullptr), #endif #ifdef ENABLE_ACTIVITIES m_activityResourceInstance(nullptr), #endif m_applicationIsReady(true), m_webConnect(new WebConnect(app)) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); struct storageInfo { eKMyMoney::StorageType type {eKMyMoney::StorageType::None}; bool isOpened {false}; QUrl url; }; storageInfo m_storageInfo; /** * The public interface. */ KMyMoneyApp * const q; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * The following variable represents the state while crafting a backup. * It can have the following values * * - IDLE: the default value if not performing a backup * - MOUNTING: when a mount command has been issued * - COPYING: when a copy command has been issued * - UNMOUNTING: when an unmount command has been issued */ backupStateE m_backupState; /** * This variable keeps the result of the backup operation. */ int m_backupResult; /** * This variable is set, when the user selected to mount/unmount * the backup volume. */ bool m_backupMount; /** * Flag for internal run control */ bool m_ignoreBackupExitCode; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; KRecentFilesAction* m_recentFiles; #ifdef ENABLE_HOLIDAYS // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif #ifdef ENABLE_ACTIVITIES KActivities::ResourceInstance * m_activityResourceInstance; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; WebConnect* m_webConnect; // methods void consistencyCheck(bool alwaysDisplayResults); static void setThemedCSS(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); void checkAccountName(const MyMoneyAccount& _acc, const QString& name) const { auto file = MyMoneyFile::instance(); if (_acc.name() != name) { MyMoneyAccount acc(_acc); acc.setName(name); file->modifyAccount(acc); } } /** * This method updates names of currencies from file to localized names */ void updateCurrencyNames() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QList storedCurrencies = MyMoneyFile::instance()->currencyList(); QList availableCurrencies = MyMoneyFile::instance()->availableCurrencyList(); QStringList currencyIDs; foreach (auto currency, availableCurrencies) currencyIDs.append(currency.id()); try { foreach (auto currency, storedCurrencies) { int i = currencyIDs.indexOf(currency.id()); if (i != -1 && availableCurrencies.at(i).name() != currency.name()) { currency.setName(availableCurrencies.at(i).name()); file->modifyCurrency(currency); } } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s updating currency names", e.what()); } } void updateAccountNames() { // make sure we setup the name of the base accounts in translated form try { MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); checkAccountName(file->asset(), i18n("Asset")); checkAccountName(file->liability(), i18n("Liability")); checkAccountName(file->income(), i18n("Income")); checkAccountName(file->expense(), i18n("Expense")); checkAccountName(file->equity(), i18n("Equity")); ft.commit(); } catch (const MyMoneyException &) { } } void ungetString(QIODevice *qfile, char *buf, int len) { buf = &buf[len-1]; while (len--) { qfile->ungetChar(*buf--); } } bool applyFileFixes() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("General Options"); // For debugging purposes, we can turn off the automatic fix manually // by setting the entry in kmymoneyrc to true if (grp.readEntry("SkipFix", false) != true) { MyMoneyFileTransaction ft; try { // Check if we have to modify the file before we allow to work with it auto s = MyMoneyFile::instance()->storage(); while (s->fileFixVersion() < s->currentFixVersion()) { qDebug("%s", qPrintable((QString("testing fileFixVersion %1 < %2").arg(s->fileFixVersion()).arg(s->currentFixVersion())))); switch (s->fileFixVersion()) { case 0: fixFile_0(); s->setFileFixVersion(1); break; case 1: fixFile_1(); s->setFileFixVersion(2); break; case 2: fixFile_2(); s->setFileFixVersion(3); break; case 3: fixFile_3(); s->setFileFixVersion(4); break; + case 4: + fixFile_4(); + s->setFileFixVersion(5); + break; + // add new levels above. Don't forget to increase currentFixVersion() for all // the storage backends this fix applies to default: throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown fix level in input file")); } } ft.commit(); } catch (const MyMoneyException &) { MyMoneyFile::instance()->blockSignals(blocked); return false; } } else { qDebug("Skipping automatic transaction fix!"); } MyMoneyFile::instance()->blockSignals(blocked); return true; } void connectStorageToModels() { const auto file = MyMoneyFile::instance(); const auto accountsModel = Models::instance()->accountsModel(); q->connect(file, &MyMoneyFile::objectAdded, accountsModel, &AccountsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, accountsModel, &AccountsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, accountsModel, &AccountsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); const auto institutionsModel = Models::instance()->institutionsModel(); q->connect(file, &MyMoneyFile::objectAdded, institutionsModel, &InstitutionsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, institutionsModel, &InstitutionsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, institutionsModel, &InstitutionsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); const auto equitiesModel = Models::instance()->equitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, equitiesModel, &EquitiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, equitiesModel, &EquitiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, equitiesModel, &EquitiesModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); const auto securitiesModel = Models::instance()->securitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, securitiesModel, &SecuritiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, securitiesModel, &SecuritiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, securitiesModel, &SecuritiesModel::slotObjectRemoved); #ifdef ENABLE_UNFINISHEDFEATURES const auto ledgerModel = Models::instance()->ledgerModel(); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddTransaction); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifyTransaction); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveTransaction); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddSchedule); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifySchedule); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveSchedule); #endif } void disconnectStorageFromModels() { const auto file = MyMoneyFile::instance(); q->disconnect(file, nullptr, Models::instance()->accountsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->institutionsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->equitiesModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->securitiesModel(), nullptr); #ifdef ENABLE_UNFINISHEDFEATURES q->disconnect(file, nullptr, Models::instance()->ledgerModel(), nullptr); #endif } bool askAboutSaving() { const auto isFileNotSaved = q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->isEnabled(); const auto isNewFileNotSaved = m_storageInfo.isOpened && m_storageInfo.url.isEmpty(); auto fileNeedsToBeSaved = false; if (isFileNotSaved && KMyMoneySettings::autoSaveOnClose()) { fileNeedsToBeSaved = true; } else if (isFileNotSaved || isNewFileNotSaved) { switch (KMessageBox::warningYesNoCancel(q, i18n("The file has been changed, save it?"))) { case KMessageBox::ButtonCode::Yes: fileNeedsToBeSaved = true; break; case KMessageBox::ButtonCode::No: fileNeedsToBeSaved = false; break; case KMessageBox::ButtonCode::Cancel: default: return false; break; } } if (fileNeedsToBeSaved) { if (isFileNotSaved) return q->slotFileSave(); else if (isNewFileNotSaved) return q->slotFileSaveAs(); } return true; } /** * This method attaches an empty storage object to the MyMoneyFile * object. It calls removeStorage() to remove a possibly attached * storage object. */ void newStorage() { removeStorage(); auto file = MyMoneyFile::instance(); file->attachStorage(new MyMoneyStorageMgr); } /** * This method removes an attached storage from the MyMoneyFile * object. */ void removeStorage() { auto file = MyMoneyFile::instance(); auto p = file->storage(); if (p) { file->detachStorage(p); delete p; } } /** * if no base currency is defined, start the dialog and force it to be set */ void selectBaseCurrency() { auto file = MyMoneyFile::instance(); // check if we have a base currency. If not, we need to select one QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", e.what()); } if (baseId.isEmpty()) { QPointer dlg = new KCurrencyEditDlg(q); // connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotSetBaseCurrency(MyMoneySecurity))); dlg->exec(); delete dlg; } try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", e.what()); } if (!baseId.isEmpty()) { // check that all accounts have a currency QList list; file->accountList(list); QList::Iterator it; // don't forget those standard accounts list << file->asset(); list << file->liability(); list << file->income(); list << file->expense(); list << file->equity(); for (it = list.begin(); it != list.end(); ++it) { QString cid; try { if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0) cid = MyMoneyFile::instance()->currency((*it).currencyId()).id(); } catch (const MyMoneyException &e) { qDebug() << QLatin1String("Account") << (*it).id() << (*it).name() << e.what(); } if (cid.isEmpty()) { (*it).setCurrencyId(baseId); MyMoneyFileTransaction ft; try { file->modifyAccount(*it); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to setup base currency in account %s (%s): %s", qPrintable((*it).name()), qPrintable((*it).id()), e.what()); } } } } } /** * Call this to see if the MyMoneyFile contains any unsaved data. * * @retval true if any data has been modified but not saved * @retval false otherwise */ bool dirty() { if (!m_storageInfo.isOpened) return false; return MyMoneyFile::instance()->dirty(); } /* DO NOT ADD code to this function or any of it's called ones. Instead, create a new function, fixFile_n, and modify the initializeStorage() logic above to call it */ + void fixFile_4() + { + auto file = MyMoneyFile::instance(); + QList currencies = file->currencyList(); + static const QStringList symbols = { QStringLiteral("XAU"), + QStringLiteral("XPD"), + QStringLiteral("XPT"), + QStringLiteral("XAG") }; + + + foreach(auto currency, currencies) { + if (symbols.contains(currency.id())) { + if (currency.smallestAccountFraction() != currency.smallestCashFraction()) { + currency.setSmallestAccountFraction(currency.smallestCashFraction()); + file->modifyCurrency(currency); + } + } + } + } + void fixFile_3() { // make sure each storage object contains a (unique) id MyMoneyFile::instance()->storageId(); } void fixFile_2() { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); // scan the transactions and modify transactions with two splits // which reference an account and a category to have the memo text // of the account. auto count = 0; foreach (const auto transaction, transactionList) { if (transaction.splitCount() == 2) { QString accountId; QString categoryId; QString accountMemo; QString categoryMemo; foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); if (acc.isIncomeExpense()) { categoryId = split.id(); categoryMemo = split.memo(); } else { accountId = split.id(); accountMemo = split.memo(); } } if (!accountId.isEmpty() && !categoryId.isEmpty() && accountMemo != categoryMemo) { MyMoneyTransaction t(transaction); MyMoneySplit s(t.splitById(categoryId)); s.setMemo(accountMemo); t.modifySplit(s); file->modifyTransaction(t); ++count; } } } qDebug("%d transactions fixed in fixFile_2", count); } void fixFile_1() { // we need to fix reports. If the account filter list contains // investment accounts, we need to add the stock accounts to the list // as well if we don't have the expert mode enabled if (!KMyMoneySettings::expertMode()) { try { QList reports = MyMoneyFile::instance()->reportList(); QList::iterator it_r; for (it_r = reports.begin(); it_r != reports.end(); ++it_r) { QStringList list; (*it_r).accounts(list); QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } if (!missing.isEmpty()) { (*it_r).addAccount(missing); MyMoneyFile::instance()->modifyReport(*it_r); } } } catch (const MyMoneyException &) { } } } #if 0 if (!m_accountsView->allItemsSelected()) { // retrieve a list of selected accounts QStringList list; m_accountsView->selectedItems(list); // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected if (!KMyMoneySettings::expertMode()) { QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.begin(); it_a != list.end(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } list += missing; } m_filter.addAccount(list); } #endif void fixFile_0() { /* (Ace) I am on a crusade against file fixups. Whenever we have to fix the * file, it is really a warning. So I'm going to print a debug warning, and * then go track them down when I see them to figure out how they got saved * out needing fixing anyway. */ auto file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList::Iterator it_a; QList scheduleList = file->scheduleList(); QList::Iterator it_s; MyMoneyAccount equity = file->equity(); MyMoneyAccount asset = file->asset(); bool equityListEmpty = equity.accountList().count() == 0; for (it_a = accountList.begin(); it_a != accountList.end(); ++it_a) { if ((*it_a).accountType() == eMyMoney::Account::Type::Loan || (*it_a).accountType() == eMyMoney::Account::Type::AssetLoan) { fixLoanAccount_0(*it_a); } // until early before 0.8 release, the equity account was not saved to // the file. If we have an equity account with no sub-accounts but // find and equity account that has equity() as it's parent, we reparent // this account. Need to move it to asset() first, because otherwise // MyMoneyFile::reparent would act as NOP. if (equityListEmpty && (*it_a).accountType() == eMyMoney::Account::Type::Equity) { if ((*it_a).parentAccountId() == equity.id()) { auto acc = *it_a; // tricky, force parent account to be empty so that we really // can re-parent it acc.setParentAccountId(QString()); file->reparentAccount(acc, equity); qDebug() << Q_FUNC_INFO << " fixed account " << acc.id() << " reparented to " << equity.id(); } } } for (it_s = scheduleList.begin(); it_s != scheduleList.end(); ++it_s) { fixSchedule_0(*it_s); } fixTransactions_0(); } void fixSchedule_0(MyMoneySchedule sched) { MyMoneyTransaction t = sched.transaction(); QList splitList = t.splits(); QList::ConstIterator it_s; try { bool updated = false; // Check if the splits contain valid data and set it to // be valid. for (it_s = splitList.constBegin(); it_s != splitList.constEnd(); ++it_s) { // the first split is always the account on which this transaction operates // and if the transaction commodity is not set, we take this if (it_s == splitList.constBegin() && t.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << t.id() << " has no commodity"; try { auto acc = MyMoneyFile::instance()->account((*it_s).accountId()); t.setCommodity(acc.currencyId()); updated = true; } catch (const MyMoneyException &) { } } // make sure the account exists. If not, remove the split try { MyMoneyFile::instance()->account((*it_s).accountId()); } catch (const MyMoneyException &) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " removed, because account '" << (*it_s).accountId() << "' does not exist."; t.removeSplit(*it_s); updated = true; } if ((*it_s).reconcileFlag() != eMyMoney::Split::State::NotReconciled) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " should be 'not reconciled'"; MyMoneySplit split = *it_s; split.setReconcileDate(QDate()); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); t.modifySplit(split); updated = true; } // the schedule logic used to operate only on the value field. // This is now obsolete. if ((*it_s).shares().isZero() && !(*it_s).value().isZero()) { MyMoneySplit split = *it_s; split.setShares(split.value()); t.modifySplit(split); updated = true; } } // If there have been changes, update the schedule and // the engine data. if (updated) { sched.setTransaction(t); MyMoneyFile::instance()->modifySchedule(sched); } } catch (const MyMoneyException &e) { qWarning("Unable to update broken schedule: %s", e.what()); } } void fixLoanAccount_0(MyMoneyAccount acc) { if (acc.value("final-payment").isEmpty() || acc.value("term").isEmpty() || acc.value("periodic-payment").isEmpty() || acc.value("loan-amount").isEmpty() || acc.value("interest-calculation").isEmpty() || acc.value("schedule").isEmpty() || acc.value("fixed-interest").isEmpty()) { KMessageBox::information(q, i18n("

The account \"%1\" was previously created as loan account but some information is missing.

The new loan wizard will be started to collect all relevant information.

Please use KMyMoney version 0.8.7 or later and earlier than version 0.9 to correct the problem.

" , acc.name()), i18n("Account problem")); throw MYMONEYEXCEPTION_CSTRING("Fix LoanAccount0 not supported anymore"); } } void fixTransactions_0() { auto file = MyMoneyFile::instance(); QList scheduleList = file->scheduleList(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); QList::Iterator it_x; QStringList interestAccounts; KMSTATUS(i18n("Fix transactions")); q->slotStatusProgressBar(0, scheduleList.count() + transactionList.count()); int cnt = 0; // scan the schedules to find interest accounts for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) { MyMoneyTransaction t = (*it_x).transaction(); QList::ConstIterator it_s; QStringList accounts; bool hasDuplicateAccounts = false; foreach (const auto split, t.splits()) { if (accounts.contains(split.accountId())) { hasDuplicateAccounts = true; qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId(); } else { accounts << split.accountId(); } if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { if (interestAccounts.contains(split.accountId()) == 0) { interestAccounts << split.accountId(); } } } if (hasDuplicateAccounts) { fixDuplicateAccounts_0(t); } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } // scan the transactions and modify loan transactions for (auto& transaction : transactionList) { QString defaultAction; QList splits = transaction.splits(); QStringList accounts; // check if base commodity is set. if not, set baseCurrency if (transaction.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " has no base currency"; transaction.setCommodity(file->baseCurrency().id()); file->modifyTransaction(transaction); } bool isLoan = false; // Determine default action if (transaction.splitCount() == 2) { // check for transfer int accountCount = 0; MyMoneyMoney val; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { val = split.value(); accountCount++; if (acc.accountType() == eMyMoney::Account::Type::Loan || acc.accountType() == eMyMoney::Account::Type::AssetLoan) isLoan = true; } else break; } if (accountCount == 2) { if (isLoan) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer); } else { if (val.isNegative()) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); } } isLoan = false; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); MyMoneyMoney val = split.value(); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (!val.isPositive()) { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); break; } else { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); break; } } } #if 0 // Check for correct actions in transactions referencing credit cards bool needModify = false; // The action fields are actually not used anymore in the ledger view logic // so we might as well skip this whole thing here! for (it_s = splits.begin(); needModify == false && it_s != splits.end(); ++it_s) { auto acc = file->account((*it_s).accountId()); MyMoneyMoney val = (*it_s).value(); if (acc.accountType() == Account::Type::CreditCard) { if (val < 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; if (val >= 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; } } // (Ace) Extended the #endif down to cover this conditional, because as-written // it will ALWAYS be skipped. if (needModify == true) { for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { (*it_s).setAction(defaultAction); transaction.modifySplit(*it_s); file->modifyTransaction(transaction); } splits = transaction.splits(); // update local copy qDebug("Fixed credit card assignment in %s", transaction.id().data()); } #endif // Check for correct assignment of ActionInterest in all splits // and check if there are any duplicates in this transactions for (auto& split : splits) { MyMoneyAccount splitAccount = file->account(split.accountId()); if (!accounts.contains(split.accountId())) { accounts << split.accountId(); } // if this split references an interest account, the action // must be of type ActionInterest if (interestAccounts.contains(split.accountId())) { if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest"; split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } // if it does not reference an interest account, it must not be // of type ActionInterest } else { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " does not contain an interest account so it should not have ActionInterest"; split.setAction(defaultAction); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } } // check that for splits referencing an account that has // the same currency as the transactions commodity the value // and shares field are the same. if (transaction.commodity() == splitAccount.currencyId() && split.value() != split.shares()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " " << split.id() << " uses the transaction currency, but shares != value"; split.setShares(split.value()); transaction.modifySplit(split); file->modifyTransaction(transaction); } // fix the shares and values to have the correct fraction if (!splitAccount.isInvest()) { try { int fract = splitAccount.fraction(); if (split.shares() != split.shares().convert(fract)) { qDebug("adjusting fraction in %s,%s", qPrintable(transaction.id()), qPrintable(split.id())); split.setShares(split.shares().convert(fract)); split.setValue(split.value().convert(fract)); transaction.modifySplit(split); file->modifyTransaction(transaction); } } catch (const MyMoneyException &) { qDebug("Missing security '%s', split not altered", qPrintable(splitAccount.currencyId())); } } } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } q->slotStatusProgressBar(-1, -1); } void fixDuplicateAccounts_0(MyMoneyTransaction& t) { qDebug("Duplicate account in transaction %s", qPrintable(t.id())); } /** * This method is used to update the caption of the application window. * It sets the caption to "filename [modified] - KMyMoney". * * @param skipActions if true, the actions will not be updated. This * is usually onyl required by some early calls when * these widgets are not yet created (the default is false). */ void updateCaption(); void updateActions(); bool canFileSaveAs() const; bool canUpdateAllAccounts() const; void fileAction(eKMyMoney::FileAction action); }; KMyMoneyApp::KMyMoneyApp(QWidget* parent) : KXmlGuiWindow(parent), d(new Private(this)) { #ifdef KMM_DBUS new KmymoneyAdaptor(this); QDBusConnection::sessionBus().registerObject("/KMymoney", this); QDBusConnection::sessionBus().interface()->registerService( "org.kde.kmymoney-" + QString::number(platformTools::processId()), QDBusConnectionInterface::DontQueueService); #endif // Register the main engine types used as meta-objects qRegisterMetaType("MyMoneyMoney"); qRegisterMetaType("MyMoneySecurity"); #ifdef ENABLE_SQLCIPHER /* Issues: * 1) libsqlite3 loads implicitly before libsqlcipher * thus making the second one loaded but non-functional, * 2) libsqlite3 gets linked into kmymoney target implicitly * and it's not possible to unload or unlink it explicitly * * Solution: * Use e.g. dummy sqlite3_key call, so that libsqlcipher gets loaded implicitly before libsqlite3 * thus making the first one functional. * * Additional info: * 1) loading libsqlcipher explicitly doesn't solve the issue, * 2) using sqlite3_key only in sqlstorage plugin doesn't solve the issue, * 3) in a separate, minimal test case, loading libsqlite3 explicitly * with QLibrary::ExportExternalSymbolsHint makes libsqlcipher non-functional */ sqlite3_key(nullptr, nullptr, 0); #endif // preset the pointer because we need it during the course of this constructor kmymoney = this; d->m_config = KSharedConfig::openConfig(); d->setThemedCSS(); MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); QFrame* frame = new QFrame; frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); { const auto customIconRelativePath = QString(QStringLiteral("icons/hicolor/16x16/actions/account-add.png")); #ifndef IS_APPIMAGE // find where our custom icons were installed based on an custom icon that we know should exist after installation auto customIconAbsolutePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, customIconRelativePath); if (customIconAbsolutePath.isEmpty()) { qWarning("Custom icons were not found in any of the following QStandardPaths::AppDataLocation:"); for (const auto &standardPath : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)) qWarning() << standardPath; } #else // according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications // QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons QString customIconAbsolutePath; const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney/"), customIconRelativePath); if (QFile::exists(appImageAppDataLocation )) { customIconAbsolutePath = appImageAppDataLocation ; } else { qWarning("Custom icons were not found in the following location:"); qWarning() << appImageAppDataLocation ; } #endif // add our custom icons path to icons search path if (!customIconAbsolutePath.isEmpty()) { customIconAbsolutePath.chop(customIconRelativePath.length()); customIconAbsolutePath.append(QLatin1String("icons")); auto paths = QIcon::themeSearchPaths(); paths.append(customIconAbsolutePath); QIcon::setThemeSearchPaths(paths); } #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) QString themeName = QLatin1Literal("system"); // using QIcon::setThemeName on Craft build system causes icons to disappear #else QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants #endif if (!themeName.isEmpty() && themeName != QLatin1Literal("system")) // if it isn't default theme then set it QIcon::setThemeName(themeName); Icons::setIconThemeNames(QIcon::themeName()); // get whatever theme user ends up with and hope our icon names will fit that theme } initStatusBar(); pActions = initActions(); pMenus = initMenus(); d->m_myMoneyView = new KMyMoneyView; layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); // Initialize kactivities resource instance #ifdef ENABLE_ACTIVITIES d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); #endif const auto viewActions = d->m_myMoneyView->actionsToBeConnected(); actionCollection()->addActions(viewActions.values()); for (auto it = viewActions.cbegin(); it != viewActions.cend(); ++it) pActions.insert(it.key(), it.value()); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, pPlugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); setCentralWidget(frame); connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotBackupHandleEvents())); // force to show the home page if the file is closed connect(pActions[Action::ViewTransactionDetail], &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail); d->m_backupState = BACKUP_IDLE; QLocale locale; for (auto const& weekDay: locale.weekdays()) { d->m_processingDays.setBit(static_cast(weekDay)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // connect the WebConnect server connect(d->m_webConnect, &WebConnect::gotUrl, this, &KMyMoneyApp::webConnectUrl); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); d->fileAction(eKMyMoney::FileAction::Closed); } KMyMoneyApp::~KMyMoneyApp() { // delete cached objects since they are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, pPlugins, this, guiFactory()); d->removeStorage(); #ifdef ENABLE_HOLIDAYS delete d->m_holidayRegion; #endif #ifdef ENABLE_ACTIVITIES delete d->m_activityResourceInstance; #endif // make sure all settings are written to disk KMyMoneySettings::self()->save(); delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_storageInfo.url; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } QHash KMyMoneyApp::initMenus() { QHash lutMenus; const QHash menuNames { {Menu::Institution, QStringLiteral("institution_context_menu")}, {Menu::Account, QStringLiteral("account_context_menu")}, {Menu::Schedule, QStringLiteral("schedule_context_menu")}, {Menu::Category, QStringLiteral("category_context_menu")}, {Menu::Tag, QStringLiteral("tag_context_menu")}, {Menu::Payee, QStringLiteral("payee_context_menu")}, {Menu::Investment, QStringLiteral("investment_context_menu")}, {Menu::Transaction, QStringLiteral("transaction_context_menu")}, {Menu::MoveTransaction, QStringLiteral("transaction_move_menu")}, {Menu::MarkTransaction, QStringLiteral("transaction_mark_menu")}, {Menu::MarkTransactionContext, QStringLiteral("transaction_context_mark_menu")}, {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")} }; for (auto it = menuNames.cbegin(); it != menuNames.cend(); ++it) lutMenus.insert(it.key(), qobject_cast(factory()->container(it.value(), this))); return lutMenus; } QHash KMyMoneyApp::initActions() { auto aC = actionCollection(); /* Look-up table for all custom and standard actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. keyboard shortcut */ QHash lutActions; // ************* // Adding standard actions // ************* KStandardAction::openNew(this, &KMyMoneyApp::slotFileNew, aC); KStandardAction::open(this, &KMyMoneyApp::slotFileOpen, aC); d->m_recentFiles = KStandardAction::openRecent(this, &KMyMoneyApp::slotFileOpenRecent, aC); KStandardAction::save(this, &KMyMoneyApp::slotFileSave, aC); KStandardAction::saveAs(this, &KMyMoneyApp::slotFileSaveAs, aC); KStandardAction::close(this, &KMyMoneyApp::slotFileClose, aC); KStandardAction::quit(this, &KMyMoneyApp::slotFileQuit, aC); lutActions.insert(Action::Print, KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC)); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); // ************* // Adding all actions // ************* { // struct for creating useless (unconnected) QAction struct actionInfo { Action action; QString name; QString text; Icon icon; }; const QVector actionInfos { // ************* // The File menu // ************* {Action::FileBackup, QStringLiteral("file_backup"), i18n("Backup..."), Icon::Empty}, {Action::FileImportStatement, QStringLiteral("file_import_statement"), i18n("Statement file..."), Icon::Empty}, {Action::FileImportTemplate, QStringLiteral("file_import_template"), i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, QStringLiteral("file_export_template"), i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, QStringLiteral("view_personal_data"), i18n("Personal Data..."), Icon::UserProperties}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump"), i18n("Dump Memory"), Icon::Empty}, #endif {Action::FileInformation, QStringLiteral("view_file_info"), i18n("File-Information..."), Icon::DocumentProperties}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, QStringLiteral("edit_find_transaction"), i18n("Find transaction..."), Icon::EditFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, QStringLiteral("view_show_transaction_detail"), i18n("Show Transaction Detail"), Icon::ViewTransactionDetail}, {Action::ViewHideReconciled, QStringLiteral("view_hide_reconciled_transactions"), i18n("Hide reconciled transactions"), Icon::HideReconciled}, {Action::ViewHideCategories, QStringLiteral("view_hide_unused_categories"), i18n("Hide unused categories"), Icon::HideCategories}, {Action::ViewShowAll, QStringLiteral("view_show_all_accounts"), i18n("Show all accounts"), Icon::Empty}, // ********************* // The institutions menu // ********************* {Action::NewInstitution, QStringLiteral("institution_new"), i18n("New institution..."), Icon::InstitutionNew}, {Action::EditInstitution, QStringLiteral("institution_edit"), i18n("Edit institution..."), Icon::InstitutionEdit}, {Action::DeleteInstitution, QStringLiteral("institution_delete"), i18n("Delete institution..."), Icon::InstitutionDelete}, // ***************** // The accounts menu // ***************** {Action::NewAccount, QStringLiteral("account_new"), i18n("New account..."), Icon::AccountNew}, {Action::OpenAccount, QStringLiteral("account_open"), i18n("Open ledger"), Icon::ViewFinancialList}, {Action::StartReconciliation, QStringLiteral("account_reconcile"), i18n("Reconcile..."), Icon::Reconcile}, {Action::FinishReconciliation, QStringLiteral("account_reconcile_finish"), i18nc("Finish reconciliation", "Finish"), Icon::AccountFinishReconciliation}, {Action::PostponeReconciliation, QStringLiteral("account_reconcile_postpone"), i18n("Postpone reconciliation"), Icon::MediaPlaybackPause}, {Action::EditAccount, QStringLiteral("account_edit"), i18n("Edit account..."), Icon::AccountEdit}, {Action::DeleteAccount, QStringLiteral("account_delete"), i18n("Delete account..."), Icon::AccountDelete}, {Action::CloseAccount, QStringLiteral("account_close"), i18n("Close account"), Icon::AccountClose}, {Action::ReopenAccount, QStringLiteral("account_reopen"), i18n("Reopen account"), Icon::AccountReopen}, {Action::ReportAccountTransactions, QStringLiteral("account_transaction_report"), i18n("Transaction report"), Icon::ViewFinancialList}, {Action::ChartAccountBalance, QStringLiteral("account_chart"), i18n("Show balance chart..."), Icon::OfficeChartLine}, {Action::MapOnlineAccount, QStringLiteral("account_online_map"), i18n("Map account..."), Icon::NewsSubscribe}, {Action::UnmapOnlineAccount, QStringLiteral("account_online_unmap"), i18n("Unmap account..."), Icon::NewsUnsubscribe}, {Action::UpdateAccount, QStringLiteral("account_online_update"), i18n("Update account..."), Icon::AccountUpdate}, {Action::UpdateAllAccounts, QStringLiteral("account_online_update_all"), i18n("Update all accounts..."), Icon::AccountUpdateAll}, {Action::AccountCreditTransfer, QStringLiteral("account_online_new_credit_transfer"), i18n("New credit transfer"), Icon::AccountCreditTransfer}, // ******************* // The categories menu // ******************* {Action::NewCategory, QStringLiteral("category_new"), i18n("New category..."), Icon::CategoryNew}, {Action::EditCategory, QStringLiteral("category_edit"), i18n("Edit category..."), Icon::CategoryEdit}, {Action::DeleteCategory, QStringLiteral("category_delete"), i18n("Delete category..."), Icon::CategoryDelete}, // ************** // The tools menu // ************** {Action::ToolCurrencies, QStringLiteral("tools_currency_editor"), i18n("Currencies..."), Icon::ViewCurrencyList}, {Action::ToolPrices, QStringLiteral("tools_price_editor"), i18n("Prices..."), Icon::Empty}, {Action::ToolUpdatePrices, QStringLiteral("tools_update_prices"), i18n("Update Stock and Currency Prices..."), Icon::ToolUpdatePrices}, {Action::ToolConsistency, QStringLiteral("tools_consistency_check"), i18n("Consistency Check"), Icon::Empty}, {Action::ToolPerformance, QStringLiteral("tools_performancetest"), i18n("Performance-Test"), Icon::Fork}, {Action::ToolCalculator, QStringLiteral("tools_kcalc"), i18n("Calculator..."), Icon::AccessoriesCalculator}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, QStringLiteral("settings_enable_messages"), i18n("Enable all messages"), Icon::Empty}, // ************* // The help menu // ************* {Action::HelpShow, QStringLiteral("help_show_tip"), i18n("&Show tip of the day"), Icon::Tip}, // *************************** // Actions w/o main menu entry // *************************** {Action::NewTransaction, QStringLiteral("transaction_new"), i18nc("New transaction button", "New"), Icon::TransactionNew}, {Action::EditTransaction, QStringLiteral("transaction_edit"), i18nc("Edit transaction button", "Edit"), Icon::TransactionEdit}, {Action::EnterTransaction, QStringLiteral("transaction_enter"), i18nc("Enter transaction", "Enter"), Icon::DialogOK}, {Action::EditSplits, QStringLiteral("transaction_editsplits"), i18nc("Edit split button", "Edit splits"), Icon::Split}, {Action::CancelTransaction, QStringLiteral("transaction_cancel"), i18nc("Cancel transaction edit", "Cancel"), Icon::DialogCancel}, {Action::DeleteTransaction, QStringLiteral("transaction_delete"), i18nc("Delete transaction", "Delete"), Icon::EditDelete}, {Action::DuplicateTransaction, QStringLiteral("transaction_duplicate"), i18nc("Duplicate transaction", "Duplicate"), Icon::EditCopy}, {Action::MatchTransaction, QStringLiteral("transaction_match"), i18nc("Button text for match transaction", "Match"),Icon::TransactionMatch}, {Action::AcceptTransaction, QStringLiteral("transaction_accept"), i18nc("Accept 'imported' and 'matched' transaction", "Accept"), Icon::TransactionAccept}, {Action::ToggleReconciliationFlag, QStringLiteral("transaction_mark_toggle"), i18nc("Toggle reconciliation flag", "Toggle"), Icon::Empty}, {Action::MarkCleared, QStringLiteral("transaction_mark_cleared"), i18nc("Mark transaction cleared", "Cleared"), Icon::Empty}, {Action::MarkReconciled, QStringLiteral("transaction_mark_reconciled"), i18nc("Mark transaction reconciled", "Reconciled"), Icon::Empty}, {Action::MarkNotReconciled, QStringLiteral("transaction_mark_notreconciled"), i18nc("Mark transaction not reconciled", "Not reconciled"), Icon::Empty}, {Action::SelectAllTransactions, QStringLiteral("transaction_select_all"), i18nc("Select all transactions", "Select all"), Icon::Empty}, {Action::GoToAccount, QStringLiteral("transaction_goto_account"), i18n("Go to account"), Icon::GoJump}, {Action::GoToPayee, QStringLiteral("transaction_goto_payee"), i18n("Go to payee"), Icon::GoJump}, {Action::NewScheduledTransaction, QStringLiteral("transaction_create_schedule"), i18n("Create scheduled transaction..."), Icon::AppointmentNew}, {Action::AssignTransactionsNumber, QStringLiteral("transaction_assign_number"), i18n("Assign next number"), Icon::Empty}, {Action::CombineTransactions, QStringLiteral("transaction_combine"), i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::CopySplits, QStringLiteral("transaction_copy_splits"), i18n("Copy splits"), Icon::Empty}, //Investment {Action::NewInvestment, QStringLiteral("investment_new"), i18n("New investment..."), Icon::InvestmentNew}, {Action::EditInvestment, QStringLiteral("investment_edit"), i18n("Edit investment..."), Icon::InvestmentEdit}, {Action::DeleteInvestment, QStringLiteral("investment_delete"), i18n("Delete investment..."), Icon::InvestmentDelete}, {Action::UpdatePriceOnline, QStringLiteral("investment_online_price_update"), i18n("Online price update..."), Icon::InvestmentOnlinePrice}, {Action::UpdatePriceManually, QStringLiteral("investment_manual_price_update"), i18n("Manual price update..."), Icon::Empty}, //Schedule {Action::NewSchedule, QStringLiteral("schedule_new"), i18n("New scheduled transaction"), Icon::AppointmentNew}, {Action::EditSchedule, QStringLiteral("schedule_edit"), i18n("Edit scheduled transaction"), Icon::DocumentEdit}, {Action::DeleteSchedule, QStringLiteral("schedule_delete"), i18n("Delete scheduled transaction"), Icon::EditDelete}, {Action::DuplicateSchedule, QStringLiteral("schedule_duplicate"), i18n("Duplicate scheduled transaction"), Icon::EditCopy}, {Action::EnterSchedule, QStringLiteral("schedule_enter"), i18n("Enter next transaction..."), Icon::KeyEnter}, {Action::SkipSchedule, QStringLiteral("schedule_skip"), i18n("Skip next transaction..."), Icon::MediaSeekForward}, //Payees {Action::NewPayee, QStringLiteral("payee_new"), i18n("New payee"), Icon::ListAddUser}, {Action::RenamePayee, QStringLiteral("payee_rename"), i18n("Rename payee"), Icon::PayeeRename}, {Action::DeletePayee, QStringLiteral("payee_delete"), i18n("Delete payee"), Icon::ListRemoveUser}, {Action::MergePayee, QStringLiteral("payee_merge"), i18n("Merge payees"), Icon::PayeeMerge}, //Tags {Action::NewTag, QStringLiteral("tag_new"), i18n("New tag"), Icon::ListAddTag}, {Action::RenameTag, QStringLiteral("tag_rename"), i18n("Rename tag"), Icon::TagRename}, {Action::DeleteTag, QStringLiteral("tag_delete"), i18n("Delete tag"), Icon::ListRemoveTag}, //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard"), i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, QStringLiteral("debug_traces"), i18n("Debug Traces"), Icon::Empty}, #endif {Action::DebugTimers, QStringLiteral("debug_timers"), i18n("Debug Timers"), Icon::Empty}, // onlineJob actions {Action::DeleteOnlineJob, QStringLiteral("onlinejob_delete"), i18n("Remove credit transfer"), Icon::EditDelete}, {Action::EditOnlineJob, QStringLiteral("onlinejob_edit"), i18n("Edit credit transfer"), Icon::DocumentEdit}, {Action::LogOnlineJob, QStringLiteral("onlinejob_log"), i18n("Show log"), Icon::Empty}, }; for (const auto& info : actionInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(info.name); a->setText(info.text); if (info.icon != Icon::Empty) // no need to set empty icon a->setIcon(Icons::get(info.icon)); a->setEnabled(false); lutActions.insert(info.action, a); // store QAction's pointer for later processing } } { // List with slots that get connected here. Other slots get connected in e.g. appropriate views typedef void(KMyMoneyApp::*KMyMoneyAppFunc)(); const QHash actionConnections { // ************* // The File menu // ************* // {Action::FileOpenDatabase, &KMyMoneyApp::slotOpenDatabase}, // {Action::FileSaveAsDatabase, &KMyMoneyApp::slotSaveAsDatabase}, {Action::FileBackup, &KMyMoneyApp::slotBackupFile}, {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo}, #endif {Action::FileInformation, &KMyMoneyApp::slotFileInfoDialog}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts}, // ************** // The tools menu // ************** {Action::ToolCurrencies, &KMyMoneyApp::slotCurrencyDialog}, {Action::ToolPrices, &KMyMoneyApp::slotPriceDialog}, {Action::ToolUpdatePrices, &KMyMoneyApp::slotEquityPriceUpdate}, {Action::ToolConsistency, &KMyMoneyApp::slotFileConsistencyCheck}, {Action::ToolPerformance, &KMyMoneyApp::slotPerformanceTest}, // {Action::ToolSQL, &KMyMoneyApp::slotGenerateSql}, {Action::ToolCalculator, &KMyMoneyApp::slotToolsStartKCalc}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, &KMyMoneyApp::slotEnableMessages}, // ************* // The help menu // ************* {Action::HelpShow, &KMyMoneyApp::slotShowTipOfTheDay}, // *************************** // Actions w/o main menu entry // *************************** //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, &KMyMoneyApp::slotNewFeature}, {Action::DebugTraces, &KMyMoneyApp::slotToggleTraces}, #endif {Action::DebugTimers, &KMyMoneyApp::slotToggleTimers}, }; for (auto connection = actionConnections.cbegin(); connection != actionConnections.cend(); ++connection) connect(lutActions[connection.key()], &QAction::triggered, this, connection.value()); } // ************* // Setting some of added actions checkable // ************* { // Some actions are checkable, // so set them here const QVector checkableActions { Action::ViewTransactionDetail, Action::ViewHideReconciled, Action::ViewHideCategories, #ifdef KMM_DEBUG Action::DebugTraces, Action::DebugTimers, #endif Action::ViewShowAll }; for (const auto& it : checkableActions) { lutActions[it]->setCheckable(true); lutActions[it]->setEnabled(true); } } // ************* // Setting actions that are always enabled // ************* { const QVector alwaysEnabled { Action::HelpShow, Action::SettingsAllMessages, Action::ToolPerformance, Action::ToolCalculator }; for (const auto& action : alwaysEnabled) { lutActions[action]->setEnabled(true); } } // ************* // Setting keyboard shortcuts for some of added actions // ************* { const QVector> actionShortcuts { {qMakePair(Action::EditFindTransaction, Qt::CTRL + Qt::Key_F)}, {qMakePair(Action::ViewTransactionDetail, Qt::CTRL + Qt::Key_T)}, {qMakePair(Action::ViewHideReconciled, Qt::CTRL + Qt::Key_R)}, {qMakePair(Action::ViewHideCategories, Qt::CTRL + Qt::Key_U)}, {qMakePair(Action::ViewShowAll, Qt::CTRL + Qt::SHIFT + Qt::Key_A)}, {qMakePair(Action::StartReconciliation, Qt::CTRL + Qt::SHIFT + Qt::Key_R)}, {qMakePair(Action::NewTransaction, Qt::CTRL + Qt::Key_Insert)}, {qMakePair(Action::ToggleReconciliationFlag, Qt::CTRL + Qt::Key_Space)}, {qMakePair(Action::MarkCleared, Qt::CTRL + Qt::ALT+ Qt::Key_Space)}, {qMakePair(Action::MarkReconciled, Qt::CTRL + Qt::SHIFT + Qt::Key_Space)}, {qMakePair(Action::SelectAllTransactions, Qt::CTRL + Qt::Key_A)}, #ifdef KMM_DEBUG {qMakePair(Action::WizardNewUser, Qt::CTRL + Qt::Key_G)}, #endif {qMakePair(Action::AssignTransactionsNumber, Qt::CTRL + Qt::SHIFT + Qt::Key_N)} }; for(const auto& it : actionShortcuts) aC->setDefaultShortcut(lutActions[it.first], it.second); } // ************* // Misc settings // ************* connect(onlineJobAdministration::instance(), &onlineJobAdministration::canSendCreditTransferChanged, lutActions.value(Action::AccountCreditTransfer), &QAction::setEnabled); // Setup transaction detail switch lutActions[Action::ViewTransactionDetail]->setChecked(KMyMoneySettings::showRegisterDetailed()); lutActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); lutActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); lutActions[Action::ViewShowAll]->setChecked(KMyMoneySettings::showAllAccounts()); // ************* // Adding actions to ActionCollection // ************* actionCollection()->addActions(lutActions.values()); // ************************ // Currently unused actions // ************************ #if 0 new KToolBarPopupAction(i18n("View back"), "go-previous", 0, this, SLOT(slotShowPreviousView()), actionCollection(), "go_back"); new KToolBarPopupAction(i18n("View forward"), "go-next", 0, this, SLOT(slotShowNextView()), actionCollection(), "go_forward"); action("go_back")->setEnabled(false); action("go_forward")->setEnabled(false); #endif // use the absolute path to your kmymoneyui.rc file for testing purpose in createGUI(); setupGUI(); // reconnect about app entry to dialog with full credits information auto aboutApp = aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::AboutApp))); aboutApp->disconnect(); connect(aboutApp, &QAction::triggered, this, &KMyMoneyApp::slotShowCredits); QMenu *menuContainer; menuContainer = static_cast(factory()->container(QStringLiteral("import"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentImport)); menuContainer = static_cast(factory()->container(QStringLiteral("export"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentExport)); return lutActions; } #ifdef KMM_DEBUG void KMyMoneyApp::dumpActions() const { const QList list = actionCollection()->actions(); foreach (const auto it, list) std::cout << qPrintable(it->objectName()) << ": " << qPrintable(it->text()) << std::endl; } #endif bool KMyMoneyApp::isActionToggled(const Action _a) { return pActions[_a]->isChecked(); } void KMyMoneyApp::initStatusBar() { /////////////////////////////////////////////////////////////////// // STATUSBAR d->m_statusLabel = new QLabel; statusBar()->addWidget(d->m_statusLabel); ready(); // Initialization of progress bar taken from KDevelop ;-) d->m_progressBar = new QProgressBar; statusBar()->addWidget(d->m_progressBar); d->m_progressBar->setFixedHeight(d->m_progressBar->sizeHint().height() - 8); // hide the progress bar for now slotStatusProgressBar(-1, -1); } void KMyMoneyApp::saveOptions() { KConfigGroup grp = d->m_config->group("General Options"); grp.writeEntry("Geometry", size()); grp.writeEntry("Show Statusbar", actionCollection()->action(KStandardAction::name(KStandardAction::ShowStatusbar))->isChecked()); KConfigGroup toolbarGrp = d->m_config->group("mainToolBar"); toolBar("mainToolBar")->saveSettings(toolbarGrp); d->m_recentFiles->saveEntries(d->m_config->group("Recent Files")); } void KMyMoneyApp::readOptions() { KConfigGroup grp = d->m_config->group("General Options"); pActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); pActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); d->m_recentFiles->loadEntries(d->m_config->group("Recent Files")); // Startdialog is written in the settings dialog d->m_startDialog = grp.readEntry("StartDialog", true); } #ifdef KMM_DEBUG void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); d->updateCaption(); } #endif bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (!slotFileClose()) return false; saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // MyMoneyFile::instance()->preloadCache(); } bool KMyMoneyApp::isDatabase() { return (d->m_storageInfo.isOpened && ((d->m_storageInfo.type == eKMyMoney::StorageType::SQL))); } bool KMyMoneyApp::isNativeFile() { return (d->m_storageInfo.isOpened && (d->m_storageInfo.type == eKMyMoney::StorageType::SQL || d->m_storageInfo.type == eKMyMoney::StorageType::XML)); } bool KMyMoneyApp::fileOpen() const { return d->m_storageInfo.isOpened; } KMyMoneyAppCallback KMyMoneyApp::progressCallback() { return &KMyMoneyApp::progressCallback; } void KMyMoneyApp::consistencyCheck(bool alwaysDisplayResult) { d->consistencyCheck(alwaysDisplayResult); } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url.toLocalFile())) { result = true; break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == pPlugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url.path())) result = true; // Place code here to test for QIF and other locally-supported formats // (i.e. not a plugin). If you add them here, be sure to add it to // the webConnect function. return result; } bool KMyMoneyApp::isFileOpenedInAnotherInstance(const QUrl &url) { const auto instances = instanceList(); #ifdef KMM_DBUS // check if there are other instances which might have this file open for (const auto& instance : instances) { QDBusInterface remoteApp(instance, "/KMymoney", "org.kde.kmymoney"); QDBusReply reply = remoteApp.call("filename"); if (!reply.isValid()) qDebug("D-Bus error while calling app->filename()"); else if (reply.value() == url.url()) return true; } #else Q_UNUSED(url) #endif return false; } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneySettings::setHideReconciledTransactions(pActions[Action::ViewHideReconciled]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneySettings::setHideUnusedCategory(pActions[Action::ViewHideCategories]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { KMyMoneySettings::setShowAllAccounts(pActions[Action::ViewShowAll]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } #ifdef KMM_DEBUG void KMyMoneyApp::slotFileFileInfo() { if (!d->m_storageInfo.isOpened) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, MyMoneyFile::instance()->storage()); g.close(); } void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(pActions[Action::DebugTraces]->isChecked() ? 1 : 0); } #endif void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = pActions[Action::DebugTimers]->isChecked(); } QString KMyMoneyApp::slotStatusMsg(const QString &text) { /////////////////////////////////////////////////////////////////// // change status message permanently QString previousMessage = d->m_statusLabel->text(); d->m_applicationIsReady = false; QString currentMessage = text; if (currentMessage.isEmpty() || currentMessage == i18nc("Application is ready to use", "Ready.")) { d->m_applicationIsReady = true; currentMessage = i18nc("Application is ready to use", "Ready."); } statusBar()->clearMessage(); d->m_statusLabel->setText(currentMessage); return previousMessage; } void KMyMoneyApp::ready() { slotStatusMsg(QString()); } bool KMyMoneyApp::isReady() { return d->m_applicationIsReady; } void KMyMoneyApp::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset if (d->m_progressTimer) { d->m_progressTimer->start(500); // remove from screen in 500 msec d->m_progressBar->setValue(d->m_progressBar->maximum()); } } else if (total != 0) { // init d->m_progressTimer->stop(); d->m_progressBar->setMaximum(total); d->m_progressBar->setValue(0); d->m_progressBar->show(); } else { // update QTime currentTime = QTime::currentTime(); // only process painting if last update is at least 250 ms ago if (abs(d->m_lastUpdate.msecsTo(currentTime)) > 250) { d->m_progressBar->setValue(current); d->m_lastUpdate = currentTime; } } } void KMyMoneyApp::slotStatusProgressDone() { d->m_progressTimer->stop(); d->m_progressBar->reset(); d->m_progressBar->hide(); d->m_progressBar->setValue(0); } void KMyMoneyApp::progressCallback(int current, int total, const QString& msg) { if (!msg.isEmpty()) kmymoney->slotStatusMsg(msg); kmymoney->slotStatusProgressBar(current, total); } void KMyMoneyApp::slotFileViewPersonal() { if (!d->m_storageInfo.isOpened) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userName()); user.setAddress(editPersonalDataDlg->userStreet()); user.setCity(editPersonalDataDlg->userTown()); user.setState(editPersonalDataDlg->userCountry()); user.setPostcode(editPersonalDataDlg->userPostcode()); user.setTelephone(editPersonalDataDlg->userTelephone()); user.setEmail(editPersonalDataDlg->userEmail()); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", QString::fromLatin1(e.what()))); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); QPointer dlg = new KLoadTemplateDlg(); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { // import the account templates QList templates = dlg->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(progressCallback); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to import template(s)"), QString::fromLatin1(e.what())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir templatesDir(savePath); if (!templatesDir.exists()) templatesDir.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { QPointer dlg = new KTemplateExportDlg(this); if (dlg->exec() == QDialog::Accepted && dlg) { MyMoneyTemplate templ; templ.setTitle(dlg->title()); templ.setShortDescription(dlg->shortDescription()); templ.setLongDescription(dlg->longDescription()); templ.exportTemplate(progressCallback); templ.saveTemplate(QUrl::fromLocalFile(newName)); } delete dlg; } } } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; if (KMyMoneyUtils::fileExists(url)) { if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) reallySaveFile = false; } return reallySaveFile; } void KMyMoneyApp::slotSettings() { // if we already have an instance of the settings dialog, then use it if (KConfigDialog::showDialog("KMyMoney-Settings")) return; // otherwise, we have to create it auto dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneySettings::self()); connect(dlg, &KSettingsKMyMoney::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotShowCredits() { KAboutData aboutData = initializeCreditsData(); KAboutApplicationDialog dlg(aboutData, this); dlg.exec(); } void KMyMoneyApp::slotUpdateConfiguration(const QString &dialogName) { if(dialogName.compare(QLatin1String("Plugins")) == 0) { KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Reorganize, pPlugins, this, guiFactory()); actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(d->canFileSaveAs()); onlineJobAdministration::instance()->updateActions(); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); d->updateActions(); d->m_myMoneyView->slotRefreshViews(); return; } MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #ifdef ENABLE_UNFINISHEDFEATURES LedgerSeparator::setFirstFiscalDate(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #endif d->m_myMoneyView->updateViewType(); // update the sql storage module settings // MyMoneyStorageSql::setStartDate(KMyMoneySettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneySettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneySettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneySettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneySettings::autoSavePeriod(); // stop timer if turned off but running if (d->m_autoSaveTimer->isActive() && !d->m_autoSaveEnabled) { d->m_autoSaveTimer->stop(); } // start timer if turned on and needed but not running if (!d->m_autoSaveTimer->isActive() && d->m_autoSaveEnabled && d->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setThemedCSS(); } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_storageInfo.url.isEmpty()) return; if (!d->m_storageInfo.url.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_storageInfo.url.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBoxChecked(); d->m_proc.clearProgram(); d->m_backupState = BACKUP_MOUNTING; d->m_mountpoint = backupDlg->mountPoint(); if (d->m_backupMount) { slotBackupMount(); } else { progressCallback(0, 300, ""); #ifdef Q_OS_WIN d->m_ignoreBackupExitCode = true; QTimer::singleShot(0, this, SLOT(slotBackupHandleEvents())); #else // If we don't have to mount a device, we just issue // a dummy command to start the copy operation d->m_proc.setProgram("true"); d->m_proc.start(); #endif } } delete backupDlg; } void KMyMoneyApp::slotBackupMount() { progressCallback(0, 300, i18n("Mounting %1", d->m_mountpoint)); d->m_proc.setProgram("mount"); d->m_proc << d->m_mountpoint; d->m_proc.start(); } bool KMyMoneyApp::slotBackupWriteFile() { QFileInfo fi(d->m_storageInfo.url.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_storageInfo.url.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { return false; } } progressCallback(50, 0, i18n("Writing %1", backupfile)); d->m_proc.clearProgram(); #ifdef Q_OS_WIN d->m_proc << "cmd.exe" << "/c" << "copy" << "/b" << "/y"; d->m_proc << QDir::toNativeSeparators(d->m_storageInfo.url.toLocalFile()) << "+" << "nul" << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_storageInfo.url.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; qDebug() << "Backup cmd:" << d->m_proc.program(); d->m_proc.start(); return true; } void KMyMoneyApp::slotBackupUnmount() { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } void KMyMoneyApp::slotBackupFinish() { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } void KMyMoneyApp::slotBackupHandleEvents() { switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_ignoreBackupExitCode || (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0)) { d->m_ignoreBackupExitCode = false; d->m_backupResult = 0; if (!slotBackupWriteFile()) { d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { slotBackupUnmount(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); slotBackupFinish(); } } else { qDebug("copy exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } slotBackupFinish(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotViewSelected(View view) { KMyMoneySettings::setLastViewSelected(static_cast(view)); } void KMyMoneyApp::slotGenerateSql() { // QPointer editor = new KGenerateSqlDlg(this); // editor->setObjectName("Generate Database SQL"); // editor->exec(); // delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneySettings::externalCalculator(); // if none is present, we fall back to the default if (cmd.isEmpty()) { #if defined(Q_OS_WIN32) cmd = QLatin1String("calc"); #elif defined(Q_OS_MAC) cmd = QLatin1String("open -a Calculator"); #else cmd = QLatin1String("kcalc"); #endif } KRun::runCommand(cmd, this); } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile *file = MyMoneyFile::instance(); try { const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == eMyMoney::Account::Type::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->createAccount(newAccount, parentAccount, brokerageAccount, openingBal); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account) { KNewAccountDlg::newCategory(account, MyMoneyAccount()); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard::newAccount(account); } void KMyMoneyApp::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { MyMoneyFile* file = MyMoneyFile::instance(); // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION_CSTRING("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == eMyMoney::Account::Type::Loan || newAccount.accountType() == eMyMoney::Account::Type::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(e.what()))); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(e.what()))); } } } QList > KMyMoneyApp::Private::automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; KMSTATUS(i18n("Running automatic reconciliation")); int progressBarIndex = 0; q->slotStatusProgressBar(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } q->slotStatusProgressBar(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } q->slotStatusProgressBar(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const MyMoneySecurity &security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { q->slotStatusProgressBar(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } q->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit2(transactions); while (itTransactionSplit2.hasNext()) { const QPair &transactionSplit = itTransactionSplit2.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif q->slotStatusProgressBar(-1, -1); return result; } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to institution %2. Reason: %3

", src.name(), _dst.name(), QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyAccount& _dst) { MyMoneyAccount src(_src); MyMoneyAccount dst(_dst); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->reparentAccount(src, dst); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to %2. Reason: %3

", src.name(), dst.name(), QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void KMyMoneyApp::slotNewFeature() { } // move a stock transaction from one investment account to another void KMyMoneyApp::Private::moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void KMyMoneyApp::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); if (auto menu = dynamic_cast(w)) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::Private::updateCaption() { auto caption = m_storageInfo.url.isEmpty() && m_myMoneyView && m_storageInfo.isOpened ? i18n("Untitled") : m_storageInfo.url.fileName(); #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(q->width()).arg(q->height()); #endif q->setCaption(caption, MyMoneyFile::instance()->dirty()); } void KMyMoneyApp::Private::updateActions() { const QVector actions { Action::FilePersonalData, Action::FileInformation, Action::FileImportTemplate, Action::FileExportTemplate, #ifdef KMM_DEBUG Action::FileDump, #endif Action::EditFindTransaction, Action::NewCategory, Action::ToolCurrencies, Action::ToolPrices, Action::ToolUpdatePrices, Action::ToolConsistency, Action::ToolPerformance, Action::NewAccount, Action::NewInstitution, Action::NewSchedule }; for (const auto &action : actions) pActions[action]->setEnabled(m_storageInfo.isOpened); pActions[Action::FileBackup]->setEnabled(m_storageInfo.isOpened && m_storageInfo.type == eKMyMoney::StorageType::XML); auto aC = q->actionCollection(); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(canFileSaveAs()); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(m_storageInfo.isOpened); pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); } bool KMyMoneyApp::Private::canFileSaveAs() const { return (m_storageInfo.isOpened && (!pPlugins.storage.isEmpty() && !(pPlugins.storage.count() == 1 && pPlugins.storage.first()->storageType() == eKMyMoney::StorageType::GNC))); } void KMyMoneyApp::slotDataChanged() { d->fileAction(eKMyMoney::FileAction::Changed); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all erroneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setThemedCSS() { const QStringList CSSnames {QStringLiteral("kmymoney.css"), QStringLiteral("welcome.css")}; const QString rcDir("/html/"); QStringList defaultCSSDirs; #ifndef IS_APPIMAGE defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); #else // according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications // QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons // watch out for QStringBuilder here; for yet unknown reason it causes segmentation fault on startup const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney"), rcDir); if (QFile::exists(appImageAppDataLocation + CSSnames.first())) { defaultCSSDirs.append(appImageAppDataLocation); } else { qWarning("CSS file was not found in the following location:"); qWarning() << appImageAppDataLocation; } #endif // scan the list of directories to find the ones that really // contains all files we look for QString defaultCSSDir; foreach (const auto dir, defaultCSSDirs) { defaultCSSDir = dir; foreach (const auto CSSname, CSSnames) { QFileInfo fileInfo(defaultCSSDir + CSSname); if (!fileInfo.exists()) { defaultCSSDir.clear(); break; } } if (!defaultCSSDir.isEmpty()) { break; } } // make sure we have the local directory where the themed version is stored const QString themedCSSDir = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation).first() + rcDir; QDir().mkpath(themedCSSDir); foreach (const auto CSSname, CSSnames) { const QString defaultCSSFilename = defaultCSSDir + CSSname; QFileInfo fileInfo(defaultCSSFilename); if (fileInfo.exists()) { const QString themedCSSFilename = themedCSSDir + CSSname; QFile::remove(themedCSSFilename); if (QFile::copy(defaultCSSFilename, themedCSSFilename)) { QFile cssFile (themedCSSFilename); if (cssFile.open(QIODevice::ReadWrite)) { QTextStream cssStream(&cssFile); auto cssText = cssStream.readAll(); cssText.replace(QLatin1String("./"), defaultCSSDir, Qt::CaseSensitive); cssText.replace(QLatin1String("WindowText"), KMyMoneySettings::schemeColor(SchemeColor::WindowText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Window"), KMyMoneySettings::schemeColor(SchemeColor::WindowBackground).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("HighlightText"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlightText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Highlight"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlight).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("black"), KMyMoneySettings::schemeColor(SchemeColor::ListGrid).name(), Qt::CaseSensitive); cssStream.seek(0); cssStream << cssText; cssFile.close(); } } } } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneySettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneySettings::checkSchedulePreview()); QList scheduleList = file->scheduleList(); QList::Iterator it; eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Enter; for (it = scheduleList.begin(); (it != scheduleList.end()) && (rc != eDialogs::ScheduleResultCode::Cancel); ++it) { // Get the copy in the file because it might be modified by commitTransaction MyMoneySchedule schedule = file->schedule((*it).id()); if (schedule.autoEnter()) { try { while (!schedule.isFinished() && (schedule.adjustedNextDueDate() <= checkDate) && rc != eDialogs::ScheduleResultCode::Ignore && rc != eDialogs::ScheduleResultCode::Cancel) { rc = d->m_myMoneyView->enterSchedule(schedule, true, true); schedule = file->schedule((*it).id()); // get a copy of the modified schedule } } catch (const MyMoneyException &) { } } if (rc == eDialogs::ScheduleResultCode::Ignore) { // if the current schedule was ignored then we must make sure that the user can still enter the next scheduled transaction rc = eDialogs::ScheduleResultCode::Enter; } } } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_storageInfo.url.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_storageInfo.url; } void KMyMoneyApp::writeFilenameURL(const QUrl &url) { d->m_storageInfo.url = url; } void KMyMoneyApp::addToRecentFiles(const QUrl& url) { d->m_recentFiles->addUrl(url); } QTimer* KMyMoneyApp::autosaveTimer() { return d->m_autoSaveTimer; } WebConnect* KMyMoneyApp::webConnect() const { return d->m_webConnect; } QList KMyMoneyApp::instanceList() const { QList list; #ifdef KMM_DBUS QDBusReply reply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (reply.isValid()) { QStringList apps = reply.value(); QStringList::ConstIterator it; // build a list of service names of all running kmymoney applications without this one for (it = apps.constBegin(); it != apps.constEnd(); ++it) { // please change this method of creating a list of 'all the other kmymoney instances that are running on the system' // since assuming that D-Bus creates service names with org.kde.kmymoney-PID is an observation I don't think that it's documented somewhere if ((*it).indexOf("org.kde.kmymoney-") == 0) { uint thisProcPid = platformTools::processId(); if ((*it).indexOf(QString("org.kde.kmymoney-%1").arg(thisProcPid)) != 0) list += (*it); } } } else { qDebug("D-Bus returned the following error while obtaining instances: %s", qPrintable(reply.error().message())); } #endif return list; } void KMyMoneyApp::slotEquityPriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == QDialog::Accepted && dlg != 0) dlg->storePrices(); delete dlg; } void KMyMoneyApp::webConnectUrl(const QUrl url) { QMetaObject::invokeMethod(this, "webConnect", Qt::QueuedConnection, Q_ARG(QString, url.toLocalFile()), Q_ARG(QByteArray, QByteArray())); } void KMyMoneyApp::webConnect(const QString& sourceUrl, const QByteArray& asn_id) { // // Web connect attempts to go through the known importers and see if the file // can be importing using that method. If so, it will import it using that // plugin // Q_UNUSED(asn_id) d->m_importUrlsQueue.enqueue(sourceUrl); // only start processing if this is the only import so far if (d->m_importUrlsQueue.count() == 1) { MyMoneyStatementReader::clearResultMessages(); auto statementCount = 0; while (!d->m_importUrlsQueue.isEmpty()) { ++statementCount; // get the value of the next item from the queue // but leave it on the queue for now QString url = d->m_importUrlsQueue.head(); // Bring this window to the forefront. This method was suggested by // Lubos Lunak of the KDE core development team. //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_storageInfo.isOpened && KMessageBox::warningContinueCancel(this, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) slotFileOpen(); // only continue if the user really did open a file. if (d->m_storageInfo.isOpened) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { QList statements; if (!(*it_plugin)->import(url) && !(*it_plugin)->lastError().isEmpty()) { KMessageBox::error(this, i18n("Unable to import %1 using %2 plugin. The plugin returned the following error: %3", url, (*it_plugin)->formatName(), (*it_plugin)->lastError()), i18n("Importing error")); } break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == pPlugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url)) MyMoneyStatementReader::importStatement(url, false, progressCallback); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } KMyMoneyUtils::showStatementImportResult(MyMoneyStatementReader::resultMessages(), statementCount); } } void KMyMoneyApp::slotEnableMessages() { KMessageBox::enableAllMessages(); KMessageBox::information(this, i18n("All messages have been enabled."), i18n("All messages")); } void KMyMoneyApp::createInterfaces() { // Sets up the plugin interface KMyMoneyPlugin::pluginInterfaces().appInterface = new KMyMoneyPlugin::KMMAppInterface(this, this); KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } void KMyMoneyApp::slotAutoSave() { if (!d->m_inAutoSaving) { // store the focus widget so we can restore it after save QPointer focusWidget = qApp->focusWidget(); d->m_inAutoSaving = true; KMSTATUS(i18n("Auto saving...")); //calls slotFileSave if needed, and restart the timer //it the file is not saved, reinitializes the countdown. if (d->dirty() && d->m_autoSaveEnabled) { if (!slotFileSave() && d->m_autoSavePeriod > 0) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } } d->m_inAutoSaving = false; if (focusWidget && focusWidget != qApp->focusWidget()) { // we have a valid focus widget so restore it focusWidget->setFocus(); } } } void KMyMoneyApp::slotDateChanged() { QDateTime dt = QDateTime::currentDateTime(); QDateTime nextDay(QDate(dt.date().addDays(1)), QTime(0, 0, 0)); // +1 is to make sure that we're already in the next day when the // signal is sent (this way we also avoid setting the timer to 0) QTimer::singleShot((static_cast(dt.secsTo(nextDay)) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef ENABLE_HOLIDAYS //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; #ifdef ENABLE_HOLIDAYS if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef ENABLE_HOLIDAYS //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side auto forecastDays = KMyMoneySettings::forecastDays() + KMyMoneySettings::forecastAccountCycle(); QDate endDate = QDate::currentDate().addDays(forecastDays); //look for holidays for the next 2 years as a minimum. That should give a good margin for the cache if (endDate < QDate::currentDate().addYears(2)) endDate = QDate::currentDate().addYears(2); KHolidays::Holiday::List holidayList = d->m_holidayRegion->holidays(QDate::currentDate(), endDate); KHolidays::Holiday::List::const_iterator holiday_it; for (holiday_it = holidayList.constBegin(); holiday_it != holidayList.constEnd(); ++holiday_it) { for (QDate holidayDate = (*holiday_it).observedStartDate(); holidayDate <= (*holiday_it).observedEndDate(); holidayDate = holidayDate.addDays(1)) d->m_holidayMap.insert(holidayDate, false); } for (QDate date = QDate::currentDate(); date <= endDate; date = date.addDays(1)) { //if it is not a processing day, set it to false if (!d->m_processingDays.testBit(date.dayOfWeek())) { d->m_holidayMap.insert(date, false); } else if (!d->m_holidayMap.contains(date)) { //if it is not a holiday nor a weekend, it is a processing day d->m_holidayMap.insert(date, true); } } } #endif } bool KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); if (!slotFileClose()) return false; NewUserWizard::Wizard wizard; if (wizard.exec() != QDialog::Accepted) return false; d->m_storageInfo.isOpened = true; d->m_storageInfo.type = eKMyMoney::StorageType::None; d->m_storageInfo.url = QUrl(); try { auto storage = new MyMoneyStorageMgr; MyMoneyFile::instance()->attachStorage(storage); MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); // store the user info file->setUser(wizard.user()); // create and setup base currency file->addCurrency(wizard.baseCurrency()); file->setBaseCurrency(wizard.baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard.institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account auto acc = wizard.account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard.openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard.openingBalance()); } } // import the account templates for (auto &tmpl : wizard.templates()) tmpl.importTemplate(progressCallback); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); d->fileAction(eKMyMoney::FileAction::Opened); if (actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->isEnabled()) slotFileSaveAs(); } catch (const MyMoneyException & e) { slotFileClose(); d->removeStorage(); KMessageBox::detailedError(this, i18n("Couldn't create a new file."), e.what()); return false; } if (wizard.startSettingsAfterFinished()) slotSettings(); return true; } void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); const QVector desiredFileExtensions {eKMyMoney::StorageType::XML, eKMyMoney::StorageType::GNC}; QString fileExtensions; for (const auto &extension : desiredFileExtensions) { for (const auto &plugin : pPlugins.storage) { if (plugin->storageType() == extension) { fileExtensions += plugin->fileExtension() + QLatin1String(";;"); break; } } } if (fileExtensions.isEmpty()) { KMessageBox::error(this, i18n("Couldn't find any plugin for opening storage.")); return; } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), readLastUsedDir(), fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) slotFileOpenRecent(dialog->selectedUrls().first()); delete dialog; } bool KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); if (!url.isValid()) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); if (isFileOpenedInAnotherInstance(url)) { KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); return false; } if (url.scheme() != QLatin1String("sql") && !KMyMoneyUtils::fileExists(url)) { KMessageBox::sorry(this, i18n("

%1 is either an invalid filename or the file does not exist. You can open another file or create a new one.

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); return false; } if (d->m_storageInfo.isOpened) if (!slotFileClose()) return false; // open the database d->m_storageInfo.type = eKMyMoney::StorageType::None; for (auto &plugin : pPlugins.storage) { try { if (auto pStorage = plugin->open(url)) { MyMoneyFile::instance()->attachStorage(pStorage); d->m_storageInfo.type = plugin->storageType(); if (plugin->storageType() != eKMyMoney::StorageType::GNC) { d->m_storageInfo.url = plugin->openUrl(); writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); /* Don't use url variable after KRecentFilesAction::addUrl * as it might delete it. * More in API reference to this method */ d->m_recentFiles->addUrl(url); } d->m_storageInfo.isOpened = true; break; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Cannot open file as requested."), QString::fromLatin1(e.what())); return false; } } if(d->m_storageInfo.type == eKMyMoney::StorageType::None) { KMessageBox::error(this, i18n("Could not read your data source. Please check the KMyMoney settings that the necessary plugin is enabled.")); return false; } d->fileAction(eKMyMoney::FileAction::Opened); return true; } bool KMyMoneyApp::slotFileSave() { KMSTATUS(i18n("Saving file...")); for (const auto& plugin : pPlugins.storage) { if (plugin->storageType() == d->m_storageInfo.type) { d->consistencyCheck(false); try { if (plugin->save(d->m_storageInfo.url)) { d->fileAction(eKMyMoney::FileAction::Saved); return true; } return false; } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); return false; } } } KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); return false; } bool KMyMoneyApp::slotFileSaveAs() { KMSTATUS(i18n("Saving file as....")); QVector availableFileTypes; for (const auto& plugin : pPlugins.storage) { switch (plugin->storageType()) { case eKMyMoney::StorageType::GNC: break; default: availableFileTypes.append(plugin->storageType()); break; } } auto chosenFileType = eKMyMoney::StorageType::None; switch (availableFileTypes.count()) { case 0: KMessageBox::error(this, i18n("Couldn't find any plugin for saving storage.")); return false; case 1: chosenFileType = availableFileTypes.first(); break; default: { QPointer dlg = new KSaveAsQuestion(availableFileTypes, this); auto rc = dlg->exec(); if (dlg) { auto fileType = dlg->fileType(); delete dlg; if (rc != QDialog::Accepted) return false; chosenFileType = fileType; } } } for (const auto &plugin : pPlugins.storage) { if (chosenFileType == plugin->storageType()) { try { d->consistencyCheck(false); if (plugin->saveAs()) { d->fileAction(eKMyMoney::FileAction::Saved); d->m_storageInfo.type = plugin->storageType(); return true; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); } } } return false; } bool KMyMoneyApp::slotFileClose() { if (!d->m_storageInfo.isOpened) return true; if (!d->askAboutSaving()) return false; d->fileAction(eKMyMoney::FileAction::Closing); d->removeStorage(); d->m_storageInfo = KMyMoneyApp::Private::storageInfo(); d->fileAction(eKMyMoney::FileAction::Closed); return true; } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) { QCoreApplication::quit(); } } void KMyMoneyApp::Private::fileAction(eKMyMoney::FileAction action) { switch(action) { case eKMyMoney::FileAction::Opened: q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); applyFileFixes(); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); + // Enable save in case the fix changed the contents + q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(dirty()); updateActions(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); m_myMoneyView->slotRefreshViews(); onlineJobAdministration::instance()->updateOnlineTaskProperties(); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); #ifdef ENABLE_ACTIVITIES { // make sure that we don't store the DB password in activity QUrl url(m_storageInfo.url); url.setPassword(QString()); m_activityResourceInstance->setUri(url); } #endif // start the check for scheduled transactions that need to be // entered as soon as the event loop becomes active. QMetaObject::invokeMethod(q, "slotCheckSchedules", Qt::QueuedConnection); // make sure to catch view activations connect(m_myMoneyView, &KMyMoneyView::viewActivated, q, &KMyMoneyApp::slotViewSelected); break; case eKMyMoney::FileAction::Saved: q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_autoSaveTimer->stop(); break; case eKMyMoney::FileAction::Closing: disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); // make sure to not catch view activations anymore disconnect(m_myMoneyView, &KMyMoneyView::viewActivated, q, &KMyMoneyApp::slotViewSelected); m_myMoneyView->slotFileClosed(); // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) Models::instance()->fileClosed(); break; case eKMyMoney::FileAction::Closed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); disconnectStorageFromModels(); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); updateActions(); break; case eKMyMoney::FileAction::Changed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(true && !m_storageInfo.url.isEmpty()); // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (m_autoSaveEnabled && !m_autoSaveTimer->isActive()) { m_autoSaveTimer->setSingleShot(true); m_autoSaveTimer->start(m_autoSavePeriod * 60 * 1000); //miliseconds } pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); break; default: break; } updateCaption(); } KMStatus::KMStatus(const QString &text) : m_prevText(kmymoney->slotStatusMsg(text)) { } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d(KMyMoneySettings::logPath(), "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); d.remove(KMyMoneySettings::logPath() + QString("/%1").arg(d[i])); } } diff --git a/kmymoney/mymoney/storage/mymoneystoragemgr_p.h b/kmymoney/mymoney/storage/mymoneystoragemgr_p.h index 9e432bd0c..773e3ec24 100644 --- a/kmymoney/mymoney/storage/mymoneystoragemgr_p.h +++ b/kmymoney/mymoney/storage/mymoneystoragemgr_p.h @@ -1,620 +1,620 @@ /* * Copyright 2002-2003 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2004 Kevin Tambascio * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYSTORAGEMGR_P_H #define MYMONEYSTORAGEMGR_P_H #include "mymoneystoragemgr.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "storageenums.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytag.h" #include "mymoneypayee.h" #include "mymoneybudget.h" #include "mymoneyschedule.h" #include "mymoneyreport.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneycostcenter.h" #include "mymoneymap.h" #include "onlinejob.h" #include "mymoneyenums.h" using namespace eStorage; const int INSTITUTION_ID_SIZE = 6; const int ACCOUNT_ID_SIZE = 6; const int TRANSACTION_ID_SIZE = 18; const int PAYEE_ID_SIZE = 6; const int TAG_ID_SIZE = 6; const int SCHEDULE_ID_SIZE = 6; const int SECURITY_ID_SIZE = 6; const int REPORT_ID_SIZE = 6; const int BUDGET_ID_SIZE = 6; const int ONLINE_JOB_ID_SIZE = 6; const int COSTCENTER_ID_SIZE = 6; class MyMoneyStorageMgrPrivate { Q_DISABLE_COPY(MyMoneyStorageMgrPrivate) Q_DECLARE_PUBLIC(MyMoneyStorageMgr) public: explicit MyMoneyStorageMgrPrivate(MyMoneyStorageMgr* qq) : q_ptr(qq), m_nextInstitutionID(0), m_nextAccountID(0), m_nextTransactionID(0), m_nextPayeeID(0), m_nextTagID(0), m_nextScheduleID(0), m_nextSecurityID(0), m_nextReportID(0), m_nextBudgetID(0), m_nextOnlineJobID(0), m_nextCostCenterID(0), m_dirty(false), m_creationDate(QDate::currentDate()), // initialize for file fixes (see kmymoneyview.cpp) - m_currentFixVersion(4), + m_currentFixVersion(5), m_fileFixVersion(0), // default value if no fix-version in file m_transactionListFull(false) { } ~MyMoneyStorageMgrPrivate() { } void init() { // setup standard accounts MyMoneyAccount acc_l; acc_l.setAccountType(eMyMoney::Account::Type::Liability); acc_l.setName("Liability"); MyMoneyAccount liability(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability), acc_l); MyMoneyAccount acc_a; acc_a.setAccountType(eMyMoney::Account::Type::Asset); acc_a.setName("Asset"); MyMoneyAccount asset(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset), acc_a); MyMoneyAccount acc_e; acc_e.setAccountType(eMyMoney::Account::Type::Expense); acc_e.setName("Expense"); MyMoneyAccount expense(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense), acc_e); MyMoneyAccount acc_i; acc_i.setAccountType(eMyMoney::Account::Type::Income); acc_i.setName("Income"); MyMoneyAccount income(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income), acc_i); MyMoneyAccount acc_q; acc_q.setAccountType(eMyMoney::Account::Type::Equity); acc_q.setName("Equity"); MyMoneyAccount equity(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity), acc_q); QMap map; map[MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)] = asset; map[MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)] = liability; map[MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)] = income; map[MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)] = expense; map[MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)] = equity; // load account list with initial accounts m_accountList = map; } /** * This method is used to set the dirty flag and update the * date of the last modification. */ void touch() { m_dirty = true; m_lastModificationDate = QDate::currentDate(); } /** * Adjust the balance for account @a acc by the amount of shares in split @a split. * The amount is added if @a reverse is @c false, subtracted in case it is @c true. */ void adjustBalance(MyMoneyAccount& acc, const MyMoneySplit& split, bool reverse) { // in case of an investment we can't just add or subtract the // amount of the split since we don't know about stock splits. // so in the case of those stocks, we simply recalculate the balance from scratch acc.isInvest() ? acc.setBalance(calculateBalance(acc.id(), QDate())) : acc.adjustBalance(split, reverse); } /** * This method re-parents an existing account * * An exception will be thrown upon error conditions. * * @param account MyMoneyAccount reference to account to be re-parented * @param parent MyMoneyAccount reference to new parent account * @param sendNotification if true, notifications with the ids * of all modified objects are send */ void reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent, bool /* sendNotification */) { Q_Q(MyMoneyStorageMgr); QMap::ConstIterator oldParent; QMap::ConstIterator newParent; QMap::ConstIterator childAccount; // verify that accounts exist. If one does not, // an exception is thrown q->account(account.id()); q->account(parent.id()); if (!account.parentAccountId().isEmpty()) { q->account(account.parentAccountId()); oldParent = m_accountList.find(account.parentAccountId()); } if (account.accountType() == eMyMoney::Account::Type::Stock && parent.accountType() != eMyMoney::Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Cannot move a stock acocunt into a non-investment account"); newParent = m_accountList.find(parent.id()); childAccount = m_accountList.find(account.id()); MyMoneyAccount acc; if (!account.parentAccountId().isEmpty()) { acc = (*oldParent); acc.removeAccountId(account.id()); m_accountList.modify(acc.id(), acc); } parent = (*newParent); parent.addAccountId(account.id()); m_accountList.modify(parent.id(), parent); account = (*childAccount); account.setParentAccountId(parent.id()); m_accountList.modify(account.id(), account); #if 0 // make sure the type is the same as the new parent. This does not work for stock and investment if (account.accountType() != eMyMoney::Account::Type::Stock && account.accountType() != eMyMoney::Account::Type::Investment) (*childAccount).setAccountType((*newParent).accountType()); #endif } /** * This method is used to calculate the actual balance of an account * without it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney calculateBalance(const QString& id, const QDate& date) const { Q_Q(const MyMoneyStorageMgr); MyMoneyMoney balance; MyMoneyTransactionFilter filter; filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); const auto list = q->transactionList(filter); for (const auto& transaction : list) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.accountId().compare(id) != 0) continue; else if (split.action().compare(MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) == 0) balance *= split.shares(); else balance += split.shares(); } } return balance; } void removeReferences(const QString& id) { QMap::const_iterator it_r; QMap::const_iterator it_b; // remove from reports for (it_r = m_reportList.begin(); it_r != m_reportList.end(); ++it_r) { MyMoneyReport r = *it_r; r.removeReference(id); m_reportList.modify(r.id(), r); } // remove from budgets for (it_b = m_budgetList.begin(); it_b != m_budgetList.end(); ++it_b) { MyMoneyBudget b = *it_b; b.removeReference(id); m_budgetList.modify(b.id(), b); } } /** * The member variable m_nextAccountID keeps the number that will be * assigned to the next institution created. It is maintained by * nextAccountID(). */ QString nextAccountID() { QString id; id.setNum(++m_nextAccountID); id = 'A' + id.rightJustified(ACCOUNT_ID_SIZE, '0'); return id; } /** * The member variable m_nextTransactionID keeps the number that will be * assigned to the next transaction created. It is maintained by * nextTransactionID(). */ QString nextTransactionID() { QString id; id.setNum(++m_nextTransactionID); id = 'T' + id.rightJustified(TRANSACTION_ID_SIZE, '0'); return id; } /** * The member variable m_nextPayeeID keeps the number that will be * assigned to the next payee created. It is maintained by * nextPayeeID() */ QString nextPayeeID() { QString id; id.setNum(++m_nextPayeeID); id = 'P' + id.rightJustified(PAYEE_ID_SIZE, '0'); return id; } /** * The member variable m_nextTagID keeps the number that will be * assigned to the next tag created. It is maintained by * nextTagID() */ QString nextTagID() { QString id; id.setNum(++m_nextTagID); id = 'G' + id.rightJustified(TAG_ID_SIZE, '0'); return id; } /** * The member variable m_nextInstitutionID keeps the number that will be * assigned to the next institution created. It is maintained by * nextInstitutionID(). */ QString nextInstitutionID() { QString id; id.setNum(++m_nextInstitutionID); id = 'I' + id.rightJustified(INSTITUTION_ID_SIZE, '0'); return id; } /** * The member variable m_nextScheduleID keeps the number that will be * assigned to the next schedule created. It is maintained by * nextScheduleID() */ QString nextScheduleID() { QString id; id.setNum(++m_nextScheduleID); id = "SCH" + id.rightJustified(SCHEDULE_ID_SIZE, '0'); return id; } /** * The member variable m_nextSecurityID keeps the number that will be * assigned to the next security object created. It is maintained by * nextSecurityID() */ QString nextSecurityID() { QString id; id.setNum(++m_nextSecurityID); id = 'E' + id.rightJustified(SECURITY_ID_SIZE, '0'); return id; } /** * The member variable m_nextReportID keeps the number that will be * assigned to the next report object created. It is maintained by * nextReportID() */ QString nextReportID() { QString id; id.setNum(++m_nextReportID); id = 'R' + id.rightJustified(REPORT_ID_SIZE, '0'); return id; } /** * The member variable m_nextBudgetID keeps the number that will be * assigned to the next budget object created. It is maintained by * nextBudgetID() */ QString nextBudgetID() { QString id; id.setNum(++m_nextBudgetID); id = 'B' + id.rightJustified(BUDGET_ID_SIZE, '0'); return id; } /** * This member variable keeps the number that will be assigned to the * next onlineJob object created. It is maintained by nextOnlineJobID() */ QString nextOnlineJobID() { QString id; id.setNum(++m_nextOnlineJobID); id = 'O' + id.rightJustified(ONLINE_JOB_ID_SIZE, '0'); return id; } /** * This member variable keeps the number that will be assigned to the * next cost center object created. It is maintained by nextCostCenterID() */ QString nextCostCenterID() { QString id; id.setNum(++m_nextCostCenterID); id = 'C' + id.rightJustified(COSTCENTER_ID_SIZE, '0'); return id; } ulong extractId(const QRegularExpression& exp, const QString& txtId) const { const auto match = exp.match(txtId, 0, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); if (match.hasMatch()) { return match.captured(1).toULong(); } return 0; } MyMoneyStorageMgr *q_ptr; /** * This member variable keeps the User information. * @see setUser() */ MyMoneyPayee m_user; /** * The member variable m_nextInstitutionID keeps the number that will be * assigned to the next institution created. It is maintained by * nextInstitutionID(). */ ulong m_nextInstitutionID; /** * The member variable m_nextAccountID keeps the number that will be * assigned to the next institution created. It is maintained by * nextAccountID(). */ ulong m_nextAccountID; /** * The member variable m_nextTransactionID keeps the number that will be * assigned to the next transaction created. It is maintained by * nextTransactionID(). */ ulong m_nextTransactionID; /** * The member variable m_nextPayeeID keeps the number that will be * assigned to the next payee created. It is maintained by * nextPayeeID() */ ulong m_nextPayeeID; /** * The member variable m_nextTagID keeps the number that will be * assigned to the next tag created. It is maintained by * nextTagID() */ ulong m_nextTagID; /** * The member variable m_nextScheduleID keeps the number that will be * assigned to the next schedule created. It is maintained by * nextScheduleID() */ ulong m_nextScheduleID; /** * The member variable m_nextSecurityID keeps the number that will be * assigned to the next security object created. It is maintained by * nextSecurityID() */ ulong m_nextSecurityID; ulong m_nextReportID; /** * The member variable m_nextBudgetID keeps the number that will be * assigned to the next budget object created. It is maintained by * nextBudgetID() */ ulong m_nextBudgetID; /** * This member variable keeps the number that will be assigned to the * next onlineJob object created. It is maintained by nextOnlineJobID() */ ulong m_nextOnlineJobID; /** * This member variable keeps the number that will be assigned to the * next cost center object created. It is maintained by nextCostCenterID() */ ulong m_nextCostCenterID; /** * The member variable m_institutionList is the container for the * institutions known within this file. */ MyMoneyMap m_institutionList; /** * The member variable m_accountList is the container for the accounts * known within this file. */ MyMoneyMap m_accountList; /** * The member variable m_transactionList is the container for all * transactions within this file. * @see m_transactionKeys */ MyMoneyMap m_transactionList; /** * The member variable m_transactionKeys is used to convert * transaction id's into the corresponding key used in m_transactionList. * @see m_transactionList; */ MyMoneyMap m_transactionKeys; /** * A list containing all the payees that have been used */ MyMoneyMap m_payeeList; /** * A list containing all the tags that have been used */ MyMoneyMap m_tagList; /** * A list containing all the scheduled transactions */ MyMoneyMap m_scheduleList; /** * A list containing all the security information objects. Each object * can represent a stock, bond, or mutual fund. It contains a price * history that a user can add entries to. The price history will be used * to determine the cost basis for sales, as well as the source of * information for reports in a security account. */ MyMoneyMap m_securitiesList; /** * A list containing all the currency information objects. */ MyMoneyMap m_currencyList; MyMoneyMap m_reportList; /** * A list containing all the budget information objects. */ MyMoneyMap m_budgetList; MyMoneyMap m_priceList; /** * A list containing all the onlineJob information objects. */ MyMoneyMap m_onlineJobList; /** * A list containing all the cost center information objects */ MyMoneyMap m_costCenterList; /** * This member signals if the file has been modified or not */ bool m_dirty; /** * This member variable keeps the creation date of this MyMoneyStorageMgr * object. It is set during the constructor and can only be modified using * the stream read operator. */ QDate m_creationDate; /** * This member variable keeps the date of the last modification of * the MyMoneyStorageMgr object. */ QDate m_lastModificationDate; /** * This member variable contains the current fix level of application * data files. (see kmymoneyview.cpp) */ uint m_currentFixVersion; /** * This member variable contains the current fix level of the * presently open data file. (see kmymoneyview.cpp) */ uint m_fileFixVersion; /** * This member variable is set when all transactions have been read from the database. * This is would be probably the case when doing, for e.g., a full report, * or after some types of transaction search which cannot be easily implemented in SQL */ bool m_transactionListFull; }; #endif diff --git a/kmymoney/plugins/kbanking/widgets/chiptandialog.cpp b/kmymoney/plugins/kbanking/widgets/chiptandialog.cpp index ae7a08b45..807bfee16 100644 --- a/kmymoney/plugins/kbanking/widgets/chiptandialog.cpp +++ b/kmymoney/plugins/kbanking/widgets/chiptandialog.cpp @@ -1,176 +1,176 @@ /* * A tan input dialog for optical chipTan used in online banking * Copyright 2014 Christian David * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "chiptandialog.h" #include "ui_chiptandialog.h" // Qt Includes #include #include #include #include #include #include #include // Project Includes #include "kbankingsettings.h" chipTanDialog::chipTanDialog(QWidget* parent) : QDialog(parent), m_tan(""), m_accepted(true) { ui.reset(new Ui::chipTanDialog); ui->setupUi(this); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &chipTanDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &chipTanDialog::reject); connect(ui->tanInput, &QLineEdit::textEdited, this, &chipTanDialog::tanInputChanged); ui->declarativeView->setSource(QUrl(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kbanking/qml/chipTan/ChipTan.qml")))); setFlickerFieldWidth(KBankingSettings::width()); setFlickerFieldClockSetting(KBankingSettings::clocksetting()); connect(ui->decelerateButton, SIGNAL(clicked()), ui->declarativeView->rootObject(), SLOT(decelerateTransmission())); connect(ui->accelerateButton, SIGNAL(clicked()), ui->declarativeView->rootObject(), SLOT(accelerateTransmission())); connect(ui->enlargeButton, SIGNAL(clicked()), ui->declarativeView->rootObject(), SLOT(enlargeFlickerField())); connect(ui->reduceButton, SIGNAL(clicked()), ui->declarativeView->rootObject(), SLOT(reduceFlickerField())); connect(ui->declarativeView->rootObject(), SIGNAL(flickerFieldWidthChanged(int)), this, SLOT(flickerFieldWidthChanged(int))); connect(ui->declarativeView->rootObject(), SIGNAL(flickerFieldClockSettingChanged(int)), SLOT(flickerFieldClockSettingChanged(int))); if (ui->declarativeView->status() == QQuickWidget::Error) done(InternalError); tanInputChanged(QString()); ui->tanInput->setFocus(); } chipTanDialog::~chipTanDialog() { } void chipTanDialog::accept() { m_tan = ui->tanInput->text(); m_accepted = true; done(Accepted); } void chipTanDialog::reject() { m_accepted = false; done(Rejected); } void chipTanDialog::setInfoText(const QString& text) { ui->infoText->setText(text); } QString chipTanDialog::infoText() { return ui->infoText->toPlainText(); } void chipTanDialog::setHhdCode(const QString& code) { setRootObjectProperty("transferData", code); } QString chipTanDialog::hhdCode() { QQuickItem* rootObject = ui->declarativeView->rootObject(); if (rootObject) return rootObject->property("transferData").toString(); return QString(); } QString chipTanDialog::tan() { return m_tan; } void chipTanDialog::setTanLimits(const int& minLength, const int& maxLength) { ui->tanInput->setValidator(new QRegExpValidator(QRegExp(QString("\\d{%1,%2}").arg(minLength).arg(maxLength)), ui->tanInput)); } void chipTanDialog::setFlickerFieldWidth(const int& width) { QQuickItem* rootObject = ui->declarativeView->rootObject(); if (rootObject) QMetaObject::invokeMethod(rootObject, "setFlickerFieldWidth", Q_ARG(QVariant, QVariant(width))); } int chipTanDialog::flickerFieldWidth() { QQuickItem* rootObject = ui->declarativeView->rootObject(); QVariant width; if (rootObject) QMetaObject::invokeMethod(rootObject, "flickerFieldWidth", Qt::DirectConnection, Q_RETURN_ARG(QVariant, width)); return width.toInt(); } void chipTanDialog::setFlickerFieldClockSetting(const int& width) { QQuickItem *const rootObject = ui->declarativeView->rootObject(); if (rootObject) QMetaObject::invokeMethod(rootObject, "setFlickerClockSetting", Q_ARG(QVariant, QVariant(width))); } void chipTanDialog::flickerFieldClockSettingChanged(const int& takt) { KBankingSettings::setClocksetting(takt); KBankingSettings::self()->save(); } void chipTanDialog::flickerFieldWidthChanged(const int& width) { ui->declarativeView->setFixedWidth(width); KBankingSettings::setWidth(width); KBankingSettings::self()->save(); } void chipTanDialog::tanInputChanged(const QString& input) { QPushButton *const button = ui->buttonBox->button(QDialogButtonBox::Ok); Q_ASSERT(button); if (input.isEmpty() || !ui->tanInput->hasAcceptableInput()) { button->setEnabled(false); button->setToolTip(i18n("A valid tan is required to proceed.")); } else { button->setEnabled(true); - button->setToolTip(""); + button->setToolTip(QString()); } } void chipTanDialog::setRootObjectProperty(const char* property, const QVariant& value) { QQuickItem *const rootObject = ui->declarativeView->rootObject(); if (rootObject) rootObject->setProperty(property, value); } diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp index eff7e29af..0c10b9a80 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,2122 +1,2180 @@ /*************************************************************************** kgloballedgerview.cpp - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kgloballedgerview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include +#include +#include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kmymoneypayeecombo.h" #include "keditscheduledlg.h" #include "kendingbalancedlg.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "kfindtransactiondlg.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "transaction.h" #include "transactionform.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace eMenu; QDate KGlobalLedgerViewPrivate::m_lastPostDate; KGlobalLedgerView::KGlobalLedgerView(QWidget *parent) : KMyMoneyViewBase(*new KGlobalLedgerViewPrivate(this), parent) { typedef void(KGlobalLedgerView::*KGlobalLedgerViewFunc)(); const QHash actionConnections { {Action::NewTransaction, &KGlobalLedgerView::slotNewTransaction}, {Action::EditTransaction, &KGlobalLedgerView::slotEditTransaction}, {Action::DeleteTransaction, &KGlobalLedgerView::slotDeleteTransaction}, {Action::DuplicateTransaction, &KGlobalLedgerView::slotDuplicateTransaction}, {Action::EnterTransaction, &KGlobalLedgerView::slotEnterTransaction}, {Action::AcceptTransaction, &KGlobalLedgerView::slotAcceptTransaction}, {Action::CancelTransaction, &KGlobalLedgerView::slotCancelTransaction}, {Action::EditSplits, &KGlobalLedgerView::slotEditSplits}, {Action::CopySplits, &KGlobalLedgerView::slotCopySplits}, {Action::GoToPayee, &KGlobalLedgerView::slotGoToPayee}, {Action::GoToAccount, &KGlobalLedgerView::slotGoToAccount}, {Action::MatchTransaction, &KGlobalLedgerView::slotMatchTransactions}, {Action::CombineTransactions, &KGlobalLedgerView::slotCombineTransactions}, {Action::ToggleReconciliationFlag, &KGlobalLedgerView::slotToggleReconciliationFlag}, {Action::MarkCleared, &KGlobalLedgerView::slotMarkCleared}, {Action::MarkReconciled, &KGlobalLedgerView::slotMarkReconciled}, {Action::MarkNotReconciled, &KGlobalLedgerView::slotMarkNotReconciled}, {Action::SelectAllTransactions, &KGlobalLedgerView::slotSelectAllTransactions}, {Action::NewScheduledTransaction, &KGlobalLedgerView::slotCreateScheduledTransaction}, {Action::AssignTransactionsNumber, &KGlobalLedgerView::slotAssignNumber}, {Action::StartReconciliation, &KGlobalLedgerView::slotStartReconciliation}, {Action::FinishReconciliation, &KGlobalLedgerView::slotFinishReconciliation}, {Action::PostponeReconciliation, &KGlobalLedgerView::slotPostponeReconciliation}, {Action::OpenAccount, &KGlobalLedgerView::slotOpenAccount}, {Action::EditFindTransaction, &KGlobalLedgerView::slotFindTransaction}, }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); + KXmlGuiWindow* mw = KMyMoneyUtils::mainWindow(); + KStandardAction::copy(this, &KGlobalLedgerView::slotCopyTransactionToClipboard, mw->actionCollection()); + Q_D(KGlobalLedgerView); d->m_balanceWarning.reset(new KBalanceWarning(this)); } KGlobalLedgerView::~KGlobalLedgerView() { } void KGlobalLedgerView::executeCustomAction(eView::Action action) { Q_D(KGlobalLedgerView); switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: // delay the setFocus call until the event loop is running QMetaObject::invokeMethod(d->m_registerSearchLine->searchLine(), "setFocus", Qt::QueuedConnection); break; case eView::Action::DisableViewDepenedendActions: pActions[Action::SelectAllTransactions]->setEnabled(false); break; case eView::Action::InitializeAfterFileOpen: d->m_lastSelectedAccountID.clear(); d->m_currentAccount = MyMoneyAccount(); if (d->m_accountComboBox) { d->m_accountComboBox->setSelected(QString()); } break; case eView::Action::CleanupBeforeFileClose: if (d->m_inEditMode) { d->deleteTransactionEditor(); } break; default: break; } } void KGlobalLedgerView::refresh() { Q_D(KGlobalLedgerView); if (isVisible()) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; // force a new account if the current one is empty d->m_newAccountLoaded = d->m_currentAccount.id().isEmpty(); } } else { d->m_needsRefresh = true; } } void KGlobalLedgerView::showEvent(QShowEvent* event) { if (MyMoneyFile::instance()->storageAttached()) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Ledgers, eView::Action::AboutToShow); if (d->m_needsRefresh) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; d->m_newAccountLoaded = false; } } else { if (!d->m_lastSelectedAccountID.isEmpty()) { try { const auto acc = MyMoneyFile::instance()->account(d->m_lastSelectedAccountID); slotSelectAccount(acc.id()); } catch (const MyMoneyException &) { d->m_lastSelectedAccountID.clear(); // account is invalid } } else { slotSelectAccount(d->m_accountComboBox->getSelected()); } KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } } pActions[Action::SelectAllTransactions]->setEnabled(true); // don't forget base class implementation QWidget::showEvent(event); } void KGlobalLedgerView::updateActions(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); // if (typeid(obj) != typeid(MyMoneyAccount) && // (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled)) // return; const auto& acc = static_cast(obj); const QVector actionsToBeDisabled { Action::StartReconciliation, Action::FinishReconciliation, Action::PostponeReconciliation, Action::OpenAccount, Action::NewTransaction }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); auto b = acc.isClosed() ? false : true; pMenus[Menu::MoveTransaction]->setEnabled(b); QString tooltip; pActions[Action::NewTransaction]->setEnabled(canCreateTransactions(tooltip) || !isVisible()); pActions[Action::NewTransaction]->setToolTip(tooltip); const auto file = MyMoneyFile::instance(); if (!acc.id().isEmpty() && !file->isStandardAccount(acc.id())) { switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: pActions[Action::OpenAccount]->setEnabled(true); if (acc.accountGroup() != eMyMoney::Account::Type::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { pActions[Action::StartReconciliation]->setEnabled(true); pActions[Action::StartReconciliation]->setToolTip(i18n("Reconcile")); } else { auto tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); pActions[Action::StartReconciliation]->setToolTip(tip); if (!d->m_transactionEditor) { pActions[Action::FinishReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); pActions[Action::PostponeReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); } } } break; case eMyMoney::Account::Type::Income : case eMyMoney::Account::Type::Expense : pActions[Action::OpenAccount]->setEnabled(true); break; default: break; } } d->m_currentAccount = acc; // slotSelectAccount(acc); } void KGlobalLedgerView::updateLedgerActions(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->selectTransactions(list); updateLedgerActionsInternal(); } void KGlobalLedgerView::updateLedgerActionsInternal() { Q_D(KGlobalLedgerView); const QVector actionsToBeDisabled { Action::EditTransaction, Action::EditSplits, Action::EnterTransaction, Action::CancelTransaction, Action::DeleteTransaction, Action::MatchTransaction, Action::AcceptTransaction, Action::DuplicateTransaction, Action::ToggleReconciliationFlag, Action::MarkCleared, Action::GoToAccount, Action::GoToPayee, Action::AssignTransactionsNumber, Action::NewScheduledTransaction, Action::CombineTransactions, Action::CopySplits, }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); const auto file = MyMoneyFile::instance(); pActions[Action::MatchTransaction]->setText(i18nc("Button text for match transaction", "Match")); // pActions[Action::TransactionNew]->setToolTip(i18n("Create a new transaction")); pMenus[Menu::MoveTransaction]->setEnabled(false); pMenus[Menu::MarkTransaction]->setEnabled(false); pMenus[Menu::MarkTransactionContext]->setEnabled(false); if (!d->m_selectedTransactions.isEmpty() && !d->m_selectedTransactions.first().isScheduled()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } pActions[Action::DeleteTransaction]->setEnabled(enable); if (!d->m_transactionEditor) { QString tooltip = i18n("Duplicate the current selected transactions"); pActions[Action::DuplicateTransaction]->setEnabled(canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); pActions[Action::DuplicateTransaction]->setToolTip(tooltip); if (canEditTransactions(d->m_selectedTransactions, tooltip)) { pActions[Action::EditTransaction]->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { pActions[Action::EditSplits]->setEnabled(true); } if (d->m_currentAccount.isAssetLiability() && d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { pActions[Action::NewScheduledTransaction]->setEnabled(d->m_selectedTransactions.count() == 1); } } pActions[Action::EditTransaction]->setToolTip(tooltip); if (!d->m_currentAccount.isClosed()) pMenus[Menu::MoveTransaction]->setEnabled(true); pMenus[Menu::MarkTransaction]->setEnabled(true); pMenus[Menu::MarkTransactionContext]->setEnabled(true); // Allow marking the transaction if at least one is selected pActions[Action::MarkCleared]->setEnabled(true); pActions[Action::MarkReconciled]->setEnabled(true); pActions[Action::MarkNotReconciled]->setEnabled(true); pActions[Action::ToggleReconciliationFlag]->setEnabled(true); if (!d->m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && pActions[Action::TransactionEdit]->isEnabled() */) { pActions[Action::MatchTransaction]->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) pActions[Action::AcceptTransaction]->setEnabled(true); if (matchedCount != 0) { pActions[Action::MatchTransaction]->setEnabled(true); pActions[Action::MatchTransaction]->setText(i18nc("Button text for unmatch transaction", "Unmatch")); pActions[Action::MatchTransaction]->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { pActions[Action::CombineTransactions]->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } } else { pActions[Action::AssignTransactionsNumber]->setEnabled(d->m_transactionEditor->canAssignNumber()); pActions[Action::NewTransaction]->setEnabled(false); pActions[Action::DeleteTransaction]->setEnabled(false); QString reason; pActions[Action::EnterTransaction]->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan pActions[Action::EnterTransaction]->setToolTip(reason); pActions[Action::CancelTransaction]->setEnabled(true); } } } void KGlobalLedgerView::slotAboutToSelectItem(KMyMoneyRegister::RegisterItem* item, bool& okToSelect) { Q_UNUSED(item); slotCancelOrEnterTransactions(okToSelect); } void KGlobalLedgerView::slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions& selection) { Q_D(KGlobalLedgerView); if (selection.count() > 1) { MyMoneyMoney balance; foreach (const KMyMoneyRegister::SelectedTransaction& t, selection) { if (!t.isScheduled()) { balance += t.split().shares(); } } d->m_rightSummaryLabel->setText(QString("%1: %2").arg(QChar(0x2211), balance.formatMoney("", d->m_precision))); } else { if (d->isReconciliationAccount()) { d->m_rightSummaryLabel->setText(i18n("Difference: %1", d->m_totalBalance.formatMoney("", d->m_precision))); } else { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { d->m_rightSummaryLabel->setText(i18n("Balance: %1", d->m_totalBalance.formatMoney("", d->m_precision))); bool showNegative = d->m_totalBalance.isNegative(); if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_totalBalance.isZero()) showNegative = !showNegative; if (showNegative) { QPalette palette = d->m_rightSummaryLabel->palette(); palette.setColor(d->m_rightSummaryLabel->foregroundRole(), KMyMoneySettings::schemeColor(SchemeColor::Negative)); d->m_rightSummaryLabel->setPalette(palette); } } else { d->m_rightSummaryLabel->setText(i18n("Investment value: %1%2", d->m_balanceIsApproximated ? "~" : "", d->m_totalBalance.formatMoney(MyMoneyFile::instance()->baseCurrency().tradingSymbol(), d->m_precision))); } } } } void KGlobalLedgerView::resizeEvent(QResizeEvent* ev) { if (MyMoneyFile::instance()->storageAttached()) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); d->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); } KMyMoneyViewBase::resizeEvent(ev); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); if (d->m_reconciliationAccount.id() != acc.id()) { // make sure the account is selected if (!acc.id().isEmpty()) slotSelectAccount(acc.id()); d->m_reconciliationAccount = acc; d->m_reconciliationDate = reconciliationDate; d->m_endingBalance = endingBalance; if (acc.accountGroup() == eMyMoney::Account::Type::Liability) d->m_endingBalance = -endingBalance; d->m_newAccountLoaded = true; if (acc.id().isEmpty()) { d->m_buttonbar->removeAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->removeAction(pActions[Action::FinishReconciliation]); } else { d->m_buttonbar->addAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->addAction(pActions[Action::FinishReconciliation]); // when we start reconciliation, we need to reload the view // because no data has been changed. When postponing or finishing // reconciliation, the data change in the engine takes care of updating // the view. refresh(); } } } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate) { slotSetReconcileAccount(acc, reconciliationDate, MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc) { slotSetReconcileAccount(acc, QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount() { slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotShowTransactionMenu(const MyMoneySplit& sp) { Q_UNUSED(sp) pMenus[Menu::Transaction]->exec(QCursor::pos()); } void KGlobalLedgerView::slotContinueReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); MyMoneyAccount account; try { account = file->account(d->m_currentAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneySettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second, QString())); } // mark all transactions in d->m_selectedTransactions as 'Cleared' d->markTransaction(eMyMoney::Split::State::Cleared); d->m_selectedTransactions = oldSelection; } } } if (!file->isStandardAccount(account.id()) && account.isAssetLiability()) { if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); Models::instance()->accountsModel()->slotReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); slotSetReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); // check if the user requests us to create interest // or charge transactions. auto ti = d->m_endingBalanceDlg->interestTransaction(); auto tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", e.what()); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } void KGlobalLedgerView::slotLedgerSelected(const QString& _accId, const QString& transaction) { auto acc = MyMoneyFile::instance()->account(_accId); QString accId(_accId); switch (acc.accountType()) { case Account::Type::Stock: // if a stock account is selected, we show the // the corresponding parent (investment) account acc = MyMoneyFile::instance()->account(acc.parentAccountId()); accId = acc.id(); // intentional fall through case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::AssetLoan: case Account::Type::Income: case Account::Type::Expense: case Account::Type::Investment: case Account::Type::Equity: if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); slotSelectAccount(accId, transaction); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Currency: qDebug("No ledger view available for account type %d", (int)acc.accountType()); break; default: qDebug("Unknown account type %d in KMyMoneyView::slotLedgerSelected", (int)acc.accountType()); break; } } void KGlobalLedgerView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::FinishEnteringOverdueScheduledTransactions: slotContinueReconciliation(); break; case eView::Intent::SynchronizeAccountInLedgersView: slotSelectAccount(obj); break; default: break; } } void KGlobalLedgerView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch(intent) { case eView::Intent::ShowTransaction: if (variant.count() == 2) slotLedgerSelected(variant.at(0).toString(), variant.at(1).toString()); break; case eView::Intent::SelectRegisterTransactions: if (variant.count() == 1) updateLedgerActions(variant.at(0).value()); break; default: break; } } void KGlobalLedgerView::slotSelectAccount(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); if (typeid(obj) != typeid(MyMoneyAccount)) return/* false */; d->m_lastSelectedAccountID = obj.id(); } void KGlobalLedgerView::slotSelectAccount(const QString& id) { slotSelectAccount(id, QString()); } bool KGlobalLedgerView::slotSelectAccount(const QString& id, const QString& transactionId) { Q_D(KGlobalLedgerView); auto rc = true; if (!id.isEmpty()) { if (d->m_currentAccount.id() != id) { try { d->m_currentAccount = MyMoneyFile::instance()->account(id); // if a stock account is selected, we show the // the corresponding parent (investment) account if (d->m_currentAccount.isInvest()) { d->m_currentAccount = MyMoneyFile::instance()->account(d->m_currentAccount.parentAccountId()); } d->m_lastSelectedAccountID = d->m_currentAccount.id(); d->m_newAccountLoaded = true; refresh(); } catch (const MyMoneyException &) { qDebug("Unable to retrieve account %s", qPrintable(id)); rc = false; } } else { // we need to refresh m_account.m_accountList, a child could have been deleted d->m_currentAccount = MyMoneyFile::instance()->account(id); emit selectByObject(d->m_currentAccount, eView::Intent::None); emit selectByObject(d->m_currentAccount, eView::Intent::SynchronizeAccountInInvestmentView); } d->selectTransaction(transactionId); } return rc; } bool KGlobalLedgerView::selectEmptyTransaction() { Q_D(KGlobalLedgerView); bool rc = false; if (!d->m_inEditMode) { // in case we don't know the type of transaction to be created, // have at least one selected transaction and the id of // this transaction is not empty, we take it as template for the // transaction to be created KMyMoneyRegister::SelectedTransactions list(d->m_register); if ((d->m_action == eWidgets::eRegister::Action::None) && (!list.isEmpty()) && (!list[0].transaction().id().isEmpty())) { // the new transaction to be created will have the same type // as the one that currently has the focus KMyMoneyRegister::Transaction* t = dynamic_cast(d->m_register->focusItem()); if (t) d->m_action = t->actionType(); d->m_register->clearSelection(); } // if we still don't have an idea which type of transaction // to create, we use the default. if (d->m_action == eWidgets::eRegister::Action::None) { d->setupDefaultAction(); } d->m_register->selectItem(d->m_register->lastItem()); d->m_register->updateRegister(); rc = true; } return rc; } TransactionEditor* KGlobalLedgerView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); // we use the warnlevel to keep track, if we have to warn the // user that some or all splits have been reconciled or if the // user cannot modify the transaction if at least one split // has the status frozen. The following value are used: // // 0 - no sweat, user can modify // 1 - user should be warned that at least one split has been reconciled // already // 2 - user will be informed, that this transaction cannot be changed anymore int warnLevel = list.warnLevel(); Q_ASSERT(warnLevel < 2); // otherwise the edit action should not be enabled switch (warnLevel) { case 0: break; case 1: if (KMessageBox::warningContinueCancel(this, i18n( "At least one split of the selected transactions has been reconciled. " "Do you wish to continue to edit the transactions anyway?" ), i18n("Transaction already reconciled"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "EditReconciledTransaction") == KMessageBox::Cancel) { warnLevel = 2; } break; case 2: KMessageBox::sorry(this, i18n("At least one split of the selected transactions has been frozen. " "Editing the transactions is therefore prohibited."), i18n("Transaction already frozen")); break; case 3: KMessageBox::sorry(this, i18n("At least one split of the selected transaction references an account that has been closed. " "Editing the transactions is therefore prohibited."), i18n("Account closed")); break; } if (warnLevel > 1) { d->m_register->endEdit(); return 0; } TransactionEditor* editor = 0; KMyMoneyRegister::Transaction* item = dynamic_cast(d->m_register->focusItem()); if (item) { // in case the current focus item is not selected, we move the focus to the first selected transaction if (!item->isSelected()) { KMyMoneyRegister::RegisterItem* p; for (p = d->m_register->firstItem(); p; p = p->nextItem()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isSelected()) { d->m_register->setFocusItem(t); item = t; break; } } } // decide, if we edit in the register or in the form TransactionEditorContainer* parent; if (d->m_formFrame->isVisible()) parent = d->m_form; else { parent = d->m_register; } editor = item->createEditor(parent, list, KGlobalLedgerViewPrivate::m_lastPostDate); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_currentAccount)) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { if (parent == d->m_register) { // make sure, the height of the table is correct d->m_register->updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); } d->m_inEditMode = true; connect(editor, &TransactionEditor::transactionDataSufficient, pActions[Action::EnterTransaction], &QAction::setEnabled); connect(editor, &TransactionEditor::returnPressed, pActions[Action::EnterTransaction], &QAction::trigger); connect(editor, &TransactionEditor::escapePressed, pActions[Action::CancelTransaction], &QAction::trigger); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); connect(editor, &TransactionEditor::finishEdit, this, &KGlobalLedgerView::slotLeaveEditMode); connect(editor, &TransactionEditor::objectCreation, d->m_mousePressFilter, &MousePressFilter::setFilterDeactive); connect(editor, &TransactionEditor::lastPostDateUsed, this, &KGlobalLedgerView::slotKeepPostDate); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); editor->setup(d->m_tabOrderWidgets, d->m_currentAccount, d->m_action); Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // install event filter in all taborder widgets QWidgetList::const_iterator it_w = d->m_tabOrderWidgets.constBegin(); for (; it_w != d->m_tabOrderWidgets.constEnd(); ++it_w) { (*it_w)->installEventFilter(this); } // Install a filter that checks if a mouse press happened outside // of one of our own widgets. qApp->installEventFilter(d->m_mousePressFilter); // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); // for some reason, this only works reliably if delayed a bit QTimer::singleShot(10, focusWidget, SLOT(setFocus())); // preset to 'I have no idea which type to create' for the next round. d->m_action = eWidgets::eRegister::Action::None; } } return editor; } void KGlobalLedgerView::slotTransactionsContextMenuRequested() { Q_D(KGlobalLedgerView); auto transactions = d->m_selectedTransactions; updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); // that should select MyMoneySchedule in KScheduledView if (!transactions.isEmpty() && transactions.first().isScheduled()) emit selectByObject(MyMoneyFile::instance()->schedule(transactions.first().scheduleId()), eView::Intent::OpenContextMenu); else slotShowTransactionMenu(MyMoneySplit()); } void KGlobalLedgerView::slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->m_inEditMode = false; qApp->removeEventFilter(d->m_mousePressFilter); // a possible focusOut event may have removed the focus, so we // install it back again. d->m_register->focusItem()->setFocus(true); // if we come back from editing a new item, we make sure that // we always select the very last known transaction entry no // matter if the transaction has been created or not. if (list.count() && list[0].transaction().id().isEmpty()) { // block signals to prevent some infinite loops that might occur here. d->m_register->blockSignals(true); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->lastItem(); if (p && p->prevItem()) p = p->prevItem(); d->m_register->selectItem(p); d->m_register->updateRegister(true); d->m_register->blockSignals(false); // we need to update the form manually as sending signals was blocked KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) d->m_form->slotSetTransaction(t); } else { if (!KMyMoneySettings::transactionForm()) { // update the row height of the transactions because it might differ between viewing/editing mode when not using the transaction form d->m_register->blockSignals(true); d->m_register->updateRegister(true); d->m_register->blockSignals(false); } } d->m_needsRefresh = true; // TODO: Why transaction in view doesn't update without this? if (d->m_needsRefresh) refresh(); d->m_register->endEdit(); d->m_register->setFocus(); } bool KGlobalLedgerView::focusNextPrevChild(bool next) { Q_D(KGlobalLedgerView); bool rc = false; // qDebug() << "----------------------------------------------------------"; // qDebug() << "KGlobalLedgerView::focusNextPrevChild, editmode=" << d->m_inEditMode; if (d->m_inEditMode) { QWidget *w = 0; w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); const auto startIndex = currentWidgetIndex; // qDebug() << "Focus is at currentWidgetIndex" << currentWidgetIndex << w->objectName(); do { while (w && currentWidgetIndex == -1) { // qDebug() << w->objectName() << "not in list, use parent"; w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } // qDebug() << "Focus is at currentWidgetIndex" << currentWidgetIndex << w->objectName(); if (currentWidgetIndex != -1) { // if(w) qDebug() << "tab order is at" << w->objectName(); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug() << "currentWidgetIndex" << currentWidgetIndex << w->objectName() << w->isVisible(); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug() << "Set focus to" << w->objectName(); w->setFocus(); rc = true; break; } } else { break; } } while(currentWidgetIndex != startIndex); } else rc = KMyMoneyViewBase::focusNextPrevChild(next); return rc; } bool KGlobalLedgerView::eventFilter(QObject* o, QEvent* e) { Q_D(KGlobalLedgerView); bool rc = false; // Need to capture mouse position here as QEvent::ToolTip is too slow d->m_tooltipPosn = QCursor::pos(); if (e->type() == QEvent::KeyPress) { if (d->m_inEditMode) { // qDebug("object = %s, key = %d", o->className(), k->key()); if (o == d->m_register) { // we hide all key press events from the register // while editing a transaction rc = true; } } } if (!rc) rc = KMyMoneyViewBase::eventFilter(o, e); return rc; } void KGlobalLedgerView::slotSortOptions() { Q_D(KGlobalLedgerView); QPointer dlg = new KSortOptionDlg(this); QString key; QString sortOrder, def; if (d->isReconciliationAccount()) { key = "kmm-sort-reconcile"; def = KMyMoneySettings::sortReconcileView(); } else { key = "kmm-sort-std"; def = KMyMoneySettings::sortNormalView(); } // check if we have an account override of the sort order if (!d->m_currentAccount.value(key).isEmpty()) sortOrder = d->m_currentAccount.value(key); QString oldOrder = sortOrder; dlg->setSortOption(sortOrder, def); if (dlg->exec() == QDialog::Accepted) { if (dlg != 0) { sortOrder = dlg->sortOption(); if (sortOrder != oldOrder) { if (sortOrder.isEmpty()) { d->m_currentAccount.deletePair(key); } else { d->m_currentAccount.setValue(key, sortOrder); } MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to update sort order for account '%s': %s", qPrintable(d->m_currentAccount.name()), e.what()); } } } } delete dlg; } void KGlobalLedgerView::slotToggleTransactionMark(KMyMoneyRegister::Transaction* /* t */) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { slotToggleReconciliationFlag(); } } void KGlobalLedgerView::slotKeepPostDate(const QDate& date) { KGlobalLedgerViewPrivate::m_lastPostDate = date; } QString KGlobalLedgerView::accountId() const { Q_D(const KGlobalLedgerView); return d->m_currentAccount.id(); } bool KGlobalLedgerView::canCreateTransactions(QString& tooltip) const { Q_D(const KGlobalLedgerView); bool rc = true; if (d->m_currentAccount.id().isEmpty()) { tooltip = i18n("Cannot create transactions when no account is selected."); rc = false; } if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot create transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create transactions in a closed account."); d->showTooltip(tooltip); rc = false; } return rc; } bool KGlobalLedgerView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canModify(); } bool KGlobalLedgerView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canDuplicate(); } bool KGlobalLedgerView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); // check if we can edit the list of transactions. We can edit, if // // a) no mix of standard and investment transactions exist // b) if a split transaction is selected, this is the only selection // c) none of the splits is frozen // d) the transaction having the current focus is selected // check for d) if (!d->canProcessTransactions(list, tooltip)) return false; // check for c) if (list.warnLevel() == 2) { tooltip = i18n("Cannot edit transactions with frozen splits."); d->showTooltip(tooltip); return false; } bool rc = true; int investmentTransactions = 0; int normalTransactions = 0; if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot edit transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create or edit any transactions in Account %1 as it is closed", d->m_currentAccount.name()); d->showTooltip(tooltip); rc = false; } KMyMoneyRegister::SelectedTransactions::const_iterator it_t; QString action; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { if ((*it_t).transaction().id().isEmpty()) { tooltip.clear(); rc = false; continue; } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) { if (action.isEmpty()) { action = (*it_t).split().action(); continue; } if (action == (*it_t).split().action()) { continue; } else { tooltip = (i18n("Cannot edit mixed investment action/type transactions together.")); d->showTooltip(tooltip); rc = false; break; } } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) ++investmentTransactions; else ++normalTransactions; // check for a) if (investmentTransactions != 0 && normalTransactions != 0) { tooltip = i18n("Cannot edit investment transactions and non-investment transactions together."); d->showTooltip(tooltip); rc = false; break; } // check for b) but only for normalTransactions if ((*it_t).transaction().splitCount() > 2 && normalTransactions != 0) { if (list.count() > 1) { tooltip = i18n("Cannot edit multiple split transactions at once."); d->showTooltip(tooltip); rc = false; break; } } } // check for multiple transactions being selected in an investment account // we do not allow editing in this case: https://bugs.kde.org/show_bug.cgi?id=240816 // later on, we might allow to edit investment transactions of the same type /// Can now disable the following check. /* if (rc == true && investmentTransactions > 1) { tooltip = i18n("Cannot edit multiple investment transactions at once"); rc = false; }*/ // now check that we have the correct account type for investment transactions if (rc == true && investmentTransactions != 0) { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { tooltip = i18n("Cannot edit investment transactions in the context of this account."); rc = false; } } return rc; } void KGlobalLedgerView::slotMoveToAccount(const QString& id) { Q_D(KGlobalLedgerView); // close the menu, if it is still open if (pMenus[Menu::Transaction]->isVisible()) pMenus[Menu::Transaction]->close(); if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { foreach (const auto selection, d->m_selectedTransactions) { if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { d->moveInvestmentTransaction(d->m_currentAccount.id(), id, selection.transaction()); } else { auto changed = false; auto t = selection.transaction(); foreach (const auto split, selection.transaction().splits()) { if (split.accountId() == d->m_currentAccount.id()) { MyMoneySplit s = split; s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotUpdateMoveToAccountMenu() { Q_D(KGlobalLedgerView); d->createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_currentAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { accountSet.addAccountType(eMyMoney::Account::Type::Investment); } else if (d->m_currentAccount.isAssetLiability()) { accountSet.addAccountType(eMyMoney::Account::Type::Checkings); accountSet.addAccountType(eMyMoney::Account::Type::Savings); accountSet.addAccountType(eMyMoney::Account::Type::Cash); accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); accountSet.addAccountType(eMyMoney::Account::Type::Asset); accountSet.addAccountType(eMyMoney::Account::Type::Currency); accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); accountSet.addAccountType(eMyMoney::Account::Type::Loan); accountSet.addAccountType(eMyMoney::Account::Type::Liability); } else if (d->m_currentAccount.isIncomeExpense()) { accountSet.addAccountType(eMyMoney::Account::Type::Income); accountSet.addAccountType(eMyMoney::Account::Type::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference foreach (const auto selection, d->m_selectedTransactions) { foreach (const auto split, selection.transaction().splits()) { d->m_moveToAccountSelector->removeItem(split.accountId()); } } // remove those accounts from the list that are denominated // in a different currency auto list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_currentAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KGlobalLedgerView::slotObjectDestroyed(QObject* o) { Q_D(KGlobalLedgerView); if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = nullptr; } } void KGlobalLedgerView::slotCancelOrEnterTransactions(bool& okToSelect) { Q_D(KGlobalLedgerView); static bool oneTime = false; if (!oneTime) { oneTime = true; auto dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneySettings::focusChangeIsEnter() && pActions[Action::EnterTransaction]->isEnabled()) { slotEnterTransaction(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!pActions[Action::EnterTransaction]->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(pActions[Action::EnterTransaction]->toolTip()); } // in case we have a new transaction and cannot save it we simply cancel if (!pActions[Action::EnterTransaction]->isEnabled() && d->m_transactionEditor && d->m_transactionEditor->createNewTransaction()) { rc = KMessageBox::Yes; } else if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(this, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, cancelGuiItem, dontShowAgain); } else { rc = KMessageBox::warningYesNo(this, i18n("

Please select what you want to do: discard or save the changes.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotCancelTransaction(); break; case KMessageBox::No: slotEnterTransaction(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KGlobalLedgerView::slotNewSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KGlobalLedgerView::slotNewTransactionForm(eWidgets::eRegister::Action id) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { d->m_action = id; // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::NewTransaction]->isEnabled()) { if (d->createNewTransaction()) { d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor.data(), &TransactionEditor::statusProgress, this, &KGlobalLedgerView::slotStatusProgress); connect(d->m_transactionEditor.data(), &TransactionEditor::statusMsg, this, &KGlobalLedgerView::slotStatusMsg); connect(d->m_transactionEditor.data(), &TransactionEditor::scheduleTransaction, this, &KGlobalLedgerView::slotNewSchedule); } updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); } } } } } void KGlobalLedgerView::slotNewTransaction() { // in case the view is not visible ... if (!isVisible()) { // we switch to it pActions[Action::ShowLedgersView]->activate(QAction::ActionEvent::Trigger); QString tooltip; if (!canCreateTransactions(tooltip)) { // and inform the user via a dialog about the reason // why a transaction cannot be created KMessageBox::sorry(this, tooltip); return; } } slotNewTransactionForm(eWidgets::eRegister::Action::None); } void KGlobalLedgerView::slotEditTransaction() { Q_D(KGlobalLedgerView); // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditTransaction]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotDeleteTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!pActions[Action::DeleteTransaction]->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(this, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } auto msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { //KMSTATUS(i18n("Deleting transactions")); d->doDeleteTransactions(); } } void KGlobalLedgerView::slotDuplicateTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::DuplicateTransaction]->isEnabled()) { KMyMoneyRegister::SelectedTransactions selectionList = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); // KMSTATUS(i18n("Duplicating transactions")); emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { foreach (const auto selection, selectionList) { auto t = selection.transaction(); // wipe out any reconciliation information for (auto& split : t.splits()) { split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); split.setReconcileDate(QDate()); split.setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } ft.commit(); // select the new transaction in the ledger if (!d->m_currentAccount.id().isEmpty()) slotLedgerSelected(d->m_currentAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to duplicate transaction(s)"), QString::fromLatin1(e.what())); } // switch off the progress bar emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); } } void KGlobalLedgerView::slotEnterTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EnterTransaction]->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again pActions[Action::EnterTransaction]->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_currentAccount.id(); QString newId; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } d->deleteTransactionEditor(); } if (!newId.isEmpty()) { slotLedgerSelected(accountId, newId); } } updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotAcceptTransaction() { Q_D(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_currentAccount.id().isEmpty()) { foreach (const auto split, t.splits()) { if (split.accountId() == d->m_currentAccount.id()) { if (split.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { MyMoneySplit s = split; s.setReconcileFlag(eMyMoney::Split::State::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_currentAccount); matcher.accept(t, s); } emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to accept transaction"), QString::fromLatin1(e.what())); } } void KGlobalLedgerView::slotCancelTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotEditSplits() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditSplits]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify transaction"), QString::fromLatin1(e.what())); } } } d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } +void KGlobalLedgerView::slotCopyTransactionToClipboard() +{ + Q_D(KGlobalLedgerView); + + // suppress copy transactions if view not visible + // or in edit mode + if (!isVisible() || d->m_inEditMode) + return; + + // format transactions into text + QString txt; + const auto file = MyMoneyFile::instance(); + const auto acc = file->account(d->m_lastSelectedAccountID); + const auto currency = file->currency(acc.currencyId()); + + foreach (const auto& st, d->m_selectedTransactions) { + if (!txt.isEmpty() || (d->m_selectedTransactions.count() > 1)) { + txt += QStringLiteral("----------------------------\n"); + } + try { + const auto& s = st.split(); + // Date + txt += i18n("Date: %1").arg(st.transaction().postDate().toString(Qt::DefaultLocaleShortDate)); + txt += QStringLiteral("\n"); + // Payee + QString payee = i18nc("Name for unknown payee", "Unknown"); + if (!s.payeeId().isEmpty()) { + payee = file->payee(s.payeeId()).name(); + } + txt += i18n("Payee: %1").arg(payee); + txt += QStringLiteral("\n"); + // Amount + txt += i18n("Amount: %1").arg(s.value().formatMoney(currency.tradingSymbol(), MyMoneyMoney::denomToPrec(acc.fraction(currency)))); + txt += QStringLiteral("\n"); + // Memo + txt += i18n("Memo: %1").arg(s.memo()); + txt += QStringLiteral("\n"); + + } catch (MyMoneyException &) { + qDebug() << "Cannot copy transaction" << st.transaction().id() << "to clipboard"; + } + } + if (d->m_selectedTransactions.count() > 1) { + txt += QStringLiteral("----------------------------\n"); + } + + if (!txt.isEmpty()) { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(txt); + } +} + void KGlobalLedgerView::slotCopySplits() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const auto& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const auto& sourceTransaction = selectedSourceTransaction.transaction(); const auto& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { auto t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const auto& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const auto& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } // check if we need to add/update a VAT assignment file->updateVAT(t); // and store the modified transaction file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KGlobalLedgerView::slotGoToPayee() { Q_D(KGlobalLedgerView); if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_currentAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_currentAccount.id(); emit selectByVariant(QVariantList {QVariant(payeeId), QVariant(accountId), QVariant(transactionId)}, eView::Intent::ShowPayee); // emit openPayeeRequested(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotGoToAccount() { Q_D(KGlobalLedgerView); if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() slotLedgerSelected(d->m_accountGoto, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotMatchTransactions() { Q_D(KGlobalLedgerView); // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = pActions[Action::MatchTransaction]->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) d->transactionMatch(); else d->transactionUnmatch(); } void KGlobalLedgerView::slotCombineTransactions() { qDebug("slotTransactionCombine() not implemented yet"); } void KGlobalLedgerView::slotToggleReconciliationFlag() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Unknown); } void KGlobalLedgerView::slotMarkCleared() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Cleared); } void KGlobalLedgerView::slotMarkReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Reconciled); } void KGlobalLedgerView::slotMarkNotReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::NotReconciled); } void KGlobalLedgerView::slotSelectAllTransactions() { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->firstItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->isVisible() && t->isSelectable() && !t->isScheduled() && !t->id().isEmpty()) { t->setSelected(true); } } p = p->nextItem(); } // this is here only to re-paint the items without selecting anything because the data (including the selection) is not really held in the model right now d->m_register->selectAll(); // inform everyone else about the selected items KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } void KGlobalLedgerView::slotCreateScheduledTransaction() { Q_D(KGlobalLedgerView); if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); foreach (const auto split, d->m_selectedTransactions[0].transaction().splits()) { if (split.id() != splitId) { MyMoneySplit s0 = split; s0.clearId(); s0.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } KEditScheduleDlg::newSchedule(t, eMyMoney::Schedule::Occurrence::Monthly); } } void KGlobalLedgerView::slotAssignNumber() { Q_D(KGlobalLedgerView); if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KGlobalLedgerView::slotStartReconciliation() { Q_D(KGlobalLedgerView); // we cannot reconcile standard accounts if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) emit selectByObject(d->m_currentAccount, eView::Intent::StartEnteringOverdueScheduledTransactions); // asynchronous call to KScheduledView::slotEnterOverdueSchedules is made here // after that all activity should be continued in KGlobalLedgerView::slotContinueReconciliation() } void KGlobalLedgerView::slotFinishReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); auto balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) for (auto it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { auto message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(eMyMoney::TransactionFilter::Cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled for (auto it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != eMyMoney::Split::State::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); /** * This signal is emitted when an account has been successfully reconciled * and all transactions are updated in the engine. It can be used by plugins * to create reconciliation reports. * * @param account the account data * @param date the reconciliation date as provided through the dialog * @param startingBalance the starting balance as provided through the dialog * @param endingBalance the ending balance as provided through the dialog * @param transactionList reference to QList of QPair containing all * transaction/split pairs processed by the reconciliation. */ emit selectByVariant(QVariantList { QVariant::fromValue(d->m_reconciliationAccount), QVariant::fromValue(d->m_endingBalanceDlg->statementDate()), QVariant::fromValue(d->m_endingBalanceDlg->previousBalance()), QVariant::fromValue(d->m_endingBalanceDlg->endingBalance()), QVariant::fromValue(transactionList) }, eView::Intent::AccountReconciled); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); d->loadView(); // slotUpdateActions(); } void KGlobalLedgerView::slotPostponeReconciliation() { Q_D(KGlobalLedgerView); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); d->loadView(); } } void KGlobalLedgerView::slotOpenAccount() { Q_D(KGlobalLedgerView); if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) slotLedgerSelected(d->m_currentAccount.id(), QString()); } void KGlobalLedgerView::slotFindTransaction() { Q_D(KGlobalLedgerView); if (!d->m_searchDlg) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, &QObject::destroyed, this, &KGlobalLedgerView::slotCloseSearchDialog); connect(d->m_searchDlg, &KFindTransactionDlg::transactionSelected, this, &KGlobalLedgerView::slotLedgerSelected); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KGlobalLedgerView::slotCloseSearchDialog() { Q_D(KGlobalLedgerView); if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = nullptr; } void KGlobalLedgerView::slotStatusMsg(const QString& txt) { emit selectByVariant(QVariantList {QVariant(txt)}, eView::Intent::ReportProgressMessage); } void KGlobalLedgerView::slotStatusProgress(int cnt, int base) { emit selectByVariant(QVariantList {QVariant(cnt), QVariant(base)}, eView::Intent::ReportProgress); } void KGlobalLedgerView::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list) { updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } diff --git a/kmymoney/views/kgloballedgerview.h b/kmymoney/views/kgloballedgerview.h index 6a62d42da..fd523122f 100644 --- a/kmymoney/views/kgloballedgerview.h +++ b/kmymoney/views/kgloballedgerview.h @@ -1,323 +1,324 @@ /*************************************************************************** kgloballedgerview.h - description ------------------- begin : Sat Jul 13 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KGLOBALLEDGERVIEW_H #define KGLOBALLEDGERVIEW_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" class MyMoneyAccount; class MyMoneyReport; class MyMoneySplit; class MyMoneyTransaction; class TransactionEditor; namespace KMyMoneyRegister { class SelectedTransactions; } namespace KMyMoneyRegister { class RegisterItem; } namespace KMyMoneyRegister { class Transaction; } namespace KMyMoneyTransactionForm { class TransactionForm; } namespace eWidgets { namespace eRegister { enum class Action; } } namespace eMyMoney { namespace Schedule { enum class Occurrence; } } template struct QPair; template class QList; /** * @author Thomas Baumgart */ class KGlobalLedgerViewPrivate; class KGlobalLedgerView : public KMyMoneyViewBase { Q_OBJECT public: explicit KGlobalLedgerView(QWidget *parent = nullptr); ~KGlobalLedgerView() override; void executeCustomAction(eView::Action action) override; void refresh(); void updateActions(const MyMoneyObject& obj); void updateLedgerActions(const KMyMoneyRegister::SelectedTransactions& list); void updateLedgerActionsInternal(); /** * This method returns the id of the currently selected account * or QString() if none is selected. */ QString accountId() const; /** * Checks if new transactions can be created in the current context * * @param tooltip reference to string receiving the tooltip text * which explains why the modify function is not available (in case * of returning @c false) * * @retval true Yes, view allows to create transactions (tooltip is not changed) * @retval false No, view does not support creation of transactions (tooltip is updated with message) */ bool canCreateTransactions(QString& tooltip) const; /** * Checks if a list of transactions can be modified (edit/delete) in the current context * * @param list list of selected transactions * @param tooltip reference to string receiving the tooltip text * which explains why the modify function is not available (in case * of returning @c false) * * @retval true Yes, view allows to edit/delete transactions (tooltip is not changed) * @retval false No, view cannot edit/delete transactions (tooltip is updated with message) */ bool canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const; bool canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const; /** * Checks if the list of transactions can be edited in the current context * * @param list list of selected transactions * @param tooltip reference to string receiving the tooltip text * which explains why the edit function is not available (in case * of returning @c false) * * @return @c true if edit operation is possible, @c false if not */ bool canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const; TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list); /** * Method to prepare the ledger view to create a new transaction. * Returns if successful or not. * * retval true Empty transaction selected. * retval false Not successful (e.g. already editing a transaction) */ bool selectEmptyTransaction(); public Q_SLOTS: /** * This slot is used to select the correct ledger view type for * the account specified by @p id in a specific mode. * * @param accountId Internal id used for the account to show * @param transactionId Internal id used for the transaction to select. * Default is QString() which will select the last * transaction in the ledger if not the same account * * @retval true selection of account referenced by @p id succeeded * @retval false selection of account failed */ void slotSelectAccount(const QString& accountId); bool slotSelectAccount(const QString& accountId, const QString& transactionId); /** * This method is provided for convenience and acts as the method above. */ void slotSelectAccount(const MyMoneyObject& acc); /** * Switch to reconciliation mode for account @a account. * If @a account is MyMoneyAccount() (the default), reconciliation mode * is turned off. * * @param account account for which reconciliation mode is activated. * Default is MyMoneyAccount(). * @param reconciliationDate date of statement * @param endingBalance The calculated ending balance for the statement * Default ist 0. */ void slotSetReconcileAccount(const MyMoneyAccount& account, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance); void slotSetReconcileAccount(const MyMoneyAccount& account, const QDate& reconciliationDate); void slotSetReconcileAccount(const MyMoneyAccount& account); void slotSetReconcileAccount(); void slotShowTransactionMenu(const MyMoneySplit &sp); /** * Slot that should be entered after entering all due scheduled transactions * @param req is requester that made request to enter scheduled transactions * it's here to avoid reconciliation in case of random entering of scheduled transactions request */ void slotContinueReconciliation(); /** * Called, whenever the ledger view should pop up and a specific * transaction in an account should be shown. If @p transaction * is empty, the last transaction should be selected * * @param acc The ID of the account to be shown * @param transaction The ID of the transaction to be selected */ void slotLedgerSelected(const QString& _accId, const QString& transaction); void slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) override; void slotSelectByVariant(const QVariantList& variant, eView::Intent intent) override; Q_SIGNALS: /** * This signal is emitted, when a new report has been generated. A * 'generated' report is halfway between a default report and a custom * report. It's created by the system in response to the user's * request, and it's usually filtered to be a little more specific * than the usual default reports. * * The proper behaviour when getting this signal is to switch to the * reports view and display the report. But it should NOT be added * to the data file, unless the user customizes it further. That's * because the user can always come back to the ledger UI to generate * the report again. * * @param report reference to MyMoneyReport object that contains the report * details */ void reportGenerated(const MyMoneyReport& report); /** * This signal is sent out, when the current selected transaction should * be marked different */ void toggleReconciliationFlag(); protected: void showEvent(QShowEvent* event) override; void resizeEvent(QResizeEvent*) override; /** * This method handles the focus of the keyboard. When in edit mode * (m_inEditMode is true) the keyboard focus is handled * according to the widgets that are referenced in m_tabOrderWidgets. * If not in edit mode, the base class functionality is provided. * * @param next true if forward-tab, false if backward-tab was * pressed by the user */ bool focusNextPrevChild(bool next) override; bool eventFilter(QObject* o, QEvent* e) override; private: Q_DECLARE_PRIVATE(KGlobalLedgerView) private Q_SLOTS: void slotTransactionsContextMenuRequested(); void slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list); void slotSortOptions(); void slotToggleTransactionMark(KMyMoneyRegister::Transaction* t); void slotKeepPostDate(const QDate&); void slotAboutToSelectItem(KMyMoneyRegister::RegisterItem*, bool&); void slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions&); void slotMoveToAccount(const QString& id); void slotUpdateMoveToAccountMenu(); void slotObjectDestroyed(QObject* o); void slotCancelOrEnterTransactions(bool& okToSelect); void slotNewSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence); void slotNewTransactionForm(eWidgets::eRegister::Action); void slotNewTransaction(); void slotEditTransaction(); void slotDeleteTransaction(); void slotDuplicateTransaction(); void slotEnterTransaction(); /** * Accept the selected transactions that are marked as 'imported' and remove the flag */ void slotAcceptTransaction(); void slotCancelTransaction(); void slotEditSplits(); /** * This method takes the selected splits and checks that only one transaction (src) * has more than one split and all others have only a single one. It then copies the * splits of the @b src transaction to all others. */ void slotCopySplits(); void slotGoToPayee(); void slotGoToAccount(); void slotMatchTransactions(); void slotCombineTransactions(); void slotToggleReconciliationFlag(); void slotMarkCleared(); void slotMarkReconciled(); void slotMarkNotReconciled(); void slotSelectAllTransactions(); void slotCreateScheduledTransaction(); void slotAssignNumber(); + void slotCopyTransactionToClipboard(); /** * Used to start reconciliation of account @a account. It switches the * ledger view into reconciliation mode and updates the view. * * @param account account which should be reconciled * @param reconciliationDate the statement date * @param endingBalance the ending balance entered for this account * * @retval true Reconciliation started * @retval false Account cannot be reconciled */ void slotStartReconciliation(); /** * Used to finish reconciliation of account @a account. It switches the * ledger view to normal mode and updates the view. * * @param account account which should be reconciled */ void slotFinishReconciliation(); void slotPostponeReconciliation(); void slotOpenAccount(); /** * Brings up a dialog to let the user search for specific transaction(s). It then * opens a results window to display those transactions. */ void slotFindTransaction(); /** * Destroys a possibly open the search dialog */ void slotCloseSearchDialog(); void slotStatusMsg(const QString& txt); void slotStatusProgress(int cnt, int base); void slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list); }; #endif diff --git a/kmymoney/views/kgloballedgerview_p.h b/kmymoney/views/kgloballedgerview_p.h index 224612416..33dbac549 100644 --- a/kmymoney/views/kgloballedgerview_p.h +++ b/kmymoney/views/kgloballedgerview_p.h @@ -1,1654 +1,1656 @@ /*************************************************************************** kgloballedgerview_p.h - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KGLOBALLEDGERVIEW_P_H #define KGLOBALLEDGERVIEW_P_H #include "kgloballedgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "kendingbalancedlg.h" #include "kfindtransactiondlg.h" #include "kmymoneyaccountselector.h" #include "kmymoneyutils.h" #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kbalancewarning.h" #include "transactionmatcher.h" #include "tabbar.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "scheduledtransaction.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneytracer.h" #include "transaction.h" #include "transactionform.h" #include "fancydategroupmarkers.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" #include #ifdef KMM_DEBUG #include "mymoneyutils.h" #endif using namespace eMenu; using namespace eMyMoney; /** * helper class implementing an event filter to detect mouse button press * events on widgets outside a given set of widgets. This is used internally * to detect when to leave the edit mode. */ class MousePressFilter : public QObject { Q_OBJECT public: explicit MousePressFilter(QWidget* parent = nullptr) : QObject(parent), m_lastMousePressEvent(0), m_filterActive(true) { } /** * Add widget @p w to the list of possible parent objects. See eventFilter() how * they will be used. */ void addWidget(QWidget* w) { m_parents.append(w); } public Q_SLOTS: /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to activate (@a true) or deactivate (@a false) the filter */ void setFilterActive(bool state = true) { m_filterActive = state; } /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to deactivate (@a true) or activate (@a false) the filter */ void setFilterDeactive(bool state = false) { setFilterActive(!state); } protected: /** * This method checks if the widget @p child is a child of * the widget @p parent and returns either @a true or @a false. * * @param child pointer to child widget * @param parent pointer to parent widget * @retval true @p child points to widget which has @p parent as parent or grand-parent * @retval false @p child points to a widget which is not related to @p parent */ bool isChildOf(QWidget* child, QWidget* parent) { // QDialogs cannot be detected directly, but it can be assumed, // that events on a widget that do not have a parent widget within // our application are dialogs. if (!child->parentWidget()) return true; while (child) { // if we are a child of the given parent, we have a match if (child == parent) return true; // if we are at the application level, we don't have a match if (child->inherits("KMyMoneyApp")) return false; // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then // it's as if it is a child of our own because these widgets could // appear during transaction entry (message boxes, completer widgets) if (dynamic_cast(child) || ((child->windowFlags() & Qt::Popup) && /*child != kmymoney*/ !child->parentWidget())) // has no parent, then it must be top-level window return true; child = child->parentWidget(); } return false; } /** * Reimplemented from base class. Sends out the mousePressedOnExternalWidget() signal * if object @p o points to an object which is not a child widget of any added previously * using the addWidget() method. The signal is sent out only once for each event @p e. * * @param o pointer to QObject * @param e pointer to QEvent * @return always returns @a false */ bool eventFilter(QObject* o, QEvent* e) final override { if (m_filterActive) { if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) { QWidget* w = qobject_cast(o); if (!w) { return QObject::eventFilter(o, e); } QList::const_iterator it_w; for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) { if (isChildOf(w, (*it_w))) { m_lastMousePressEvent = e; break; } } if (it_w == m_parents.constEnd()) { m_lastMousePressEvent = e; bool rc = false; emit mousePressedOnExternalWidget(rc); } } if (e->type() != QEvent::MouseButtonPress) { m_lastMousePressEvent = 0; } } return false; } Q_SIGNALS: void mousePressedOnExternalWidget(bool&); private: QList m_parents; QEvent* m_lastMousePressEvent; bool m_filterActive; }; class KGlobalLedgerViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KGlobalLedgerView) public: explicit KGlobalLedgerViewPrivate(KGlobalLedgerView *qq) : q_ptr(qq), m_mousePressFilter(0), m_registerSearchLine(0), m_precision(2), m_recursion(false), m_showDetails(false), m_action(eWidgets::eRegister::Action::None), m_filterProxyModel(0), m_accountComboBox(0), m_balanceIsApproximated(false), m_toolbarFrame(nullptr), m_registerFrame(nullptr), m_buttonFrame(nullptr), m_formFrame(nullptr), m_summaryFrame(nullptr), m_register(nullptr), m_buttonbar(nullptr), m_leftSummaryLabel(nullptr), m_centerSummaryLabel(nullptr), m_rightSummaryLabel(nullptr), m_form(nullptr), m_needLoad(true), m_newAccountLoaded(true), m_inEditMode(false), m_transactionEditor(nullptr), m_balanceWarning(nullptr), m_moveToAccountSelector(nullptr), m_endingBalanceDlg(nullptr), m_searchDlg(nullptr) { } ~KGlobalLedgerViewPrivate() { delete m_moveToAccountSelector; delete m_endingBalanceDlg; delete m_searchDlg; } void init() { Q_Q(KGlobalLedgerView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); m_mousePressFilter = new MousePressFilter((QWidget*)q); m_action = eWidgets::eRegister::Action::None; // the proxy filter model m_filterProxyModel = new AccountNamesFilterProxyModel(q); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); // create the toolbar frame at the top of the view m_toolbarFrame = new QFrame(); QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame); toolbarLayout->setContentsMargins(0, 0, 0, 0); toolbarLayout->setSpacing(6); // the account selector widget m_accountComboBox = new KMyMoneyAccountCombo(); m_accountComboBox->setModel(m_filterProxyModel); toolbarLayout->addWidget(m_accountComboBox); vbox->addWidget(m_toolbarFrame); toolbarLayout->setStretchFactor(m_accountComboBox, 60); // create the register frame m_registerFrame = new QFrame(); QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame); registerFrameLayout->setContentsMargins(0, 0, 0, 0); registerFrameLayout->setSpacing(0); vbox->addWidget(m_registerFrame); vbox->setStretchFactor(m_registerFrame, 2); m_register = new KMyMoneyRegister::Register(m_registerFrame); m_register->setUsedWithEditor(true); registerFrameLayout->addWidget(m_register); m_register->installEventFilter(q); q->connect(m_register, &KMyMoneyRegister::Register::openContextMenu, q, &KGlobalLedgerView::slotTransactionsContextMenuRequested); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateSummaryLine); q->connect(m_register->horizontalHeader(), &QWidget::customContextMenuRequested, q, &KGlobalLedgerView::slotSortOptions); q->connect(m_register, &KMyMoneyRegister::Register::reconcileStateColumnClicked, q, &KGlobalLedgerView::slotToggleTransactionMark); // insert search line widget m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame); toolbarLayout->addWidget(m_registerSearchLine); toolbarLayout->setStretchFactor(m_registerSearchLine, 100); // create the summary frame m_summaryFrame = new QFrame(); QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame); summaryFrameLayout->setContentsMargins(0, 0, 0, 0); summaryFrameLayout->setSpacing(0); m_leftSummaryLabel = new QLabel(m_summaryFrame); m_centerSummaryLabel = new QLabel(m_summaryFrame); m_rightSummaryLabel = new QLabel(m_summaryFrame); summaryFrameLayout->addWidget(m_leftSummaryLabel); QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_centerSummaryLabel); spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_rightSummaryLabel); vbox->addWidget(m_summaryFrame); // create the button frame m_buttonFrame = new QFrame(q); QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame); buttonLayout->setContentsMargins(0, 0, 0, 0); buttonLayout->setSpacing(0); vbox->addWidget(m_buttonFrame); m_buttonbar = new KToolBar(m_buttonFrame, 0, true); m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); buttonLayout->addWidget(m_buttonbar); m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]); // create the transaction form frame m_formFrame = new QFrame(q); QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame); frameLayout->setContentsMargins(5, 5, 5, 5); frameLayout->setSpacing(0); m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame); frameLayout->addWidget(m_form->getTabBar(m_formFrame)); frameLayout->addWidget(m_form); m_formFrame->setFrameShape(QFrame::Panel); m_formFrame->setFrameShadow(QFrame::Raised); vbox->addWidget(m_formFrame); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::refresh); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), m_form, &KMyMoneyTransactionForm::TransactionForm::slotSetTransaction); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), q, &KGlobalLedgerView::updateLedgerActionsInternal); // q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotAccountSelected); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, static_cast(&KGlobalLedgerView::slotSelectAccount)); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotTransactionsSelected); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::editTransaction, q, &KGlobalLedgerView::slotEditTransaction); q->connect(m_register, &KMyMoneyRegister::Register::emptyItemSelected, q, &KGlobalLedgerView::slotNewTransaction); q->connect(m_register, &KMyMoneyRegister::Register::aboutToSelectItem, q, &KGlobalLedgerView::slotAboutToSelectItem); q->connect(m_mousePressFilter, &MousePressFilter::mousePressedOnExternalWidget, q, &KGlobalLedgerView::slotCancelOrEnterTransactions); q->connect(m_form, &KMyMoneyTransactionForm::TransactionForm::newTransaction, q, static_cast(&KGlobalLedgerView::slotNewTransactionForm)); // setup mouse press filter m_mousePressFilter->addWidget(m_formFrame); m_mousePressFilter->addWidget(m_buttonFrame); m_mousePressFilter->addWidget(m_summaryFrame); m_mousePressFilter->addWidget(m_registerFrame); m_tooltipPosn = QPoint(); } /** * This method reloads the account selection combo box of the * view with all asset and liability accounts from the engine. * If the account id of the current account held in @p m_accountId is * empty or if the referenced account does not exist in the engine, * the first account found in the list will be made the current account. */ void loadAccounts() { const auto file = MyMoneyFile::instance(); // check if the current account still exists and make it the // current account if (!m_lastSelectedAccountID.isEmpty()) { try { m_currentAccount = file->account(m_lastSelectedAccountID); } catch (const MyMoneyException &) { m_lastSelectedAccountID.clear(); m_currentAccount = MyMoneyAccount(); m_accountComboBox->setSelected(QString()); } } // TODO: check why the invalidate is needed here m_filterProxyModel->invalidate(); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); m_filterProxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts() && !KMyMoneySettings::showAllAccounts()); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_accountComboBox->expandAll(); if (m_currentAccount.id().isEmpty()) { // find the first favorite account QModelIndexList list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), (int)eAccountsModel::Role::Favorite, QVariant(true), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (list.count() > 0) { QVariant accountId = list.front().data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { m_currentAccount = file->account(accountId.toString()); } } if (m_currentAccount.id().isEmpty()) { // there are no favorite accounts find any account list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), Qt::DisplayRole, QVariant(QString("*")), -1, Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive)); for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!it->parent().isValid()) continue; // skip the top level accounts QVariant accountId = (*it).data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { MyMoneyAccount a = file->account(accountId.toString()); if (!a.isInvest() && !a.isClosed()) { m_currentAccount = a; break; } } } } } if (!m_currentAccount.id().isEmpty()) { m_accountComboBox->setSelected(m_currentAccount.id()); try { m_precision = MyMoneyMoney::denomToPrec(m_currentAccount.fraction()); } catch (const MyMoneyException &) { qDebug("Security %s for account %s not found", qPrintable(m_currentAccount.currencyId()), qPrintable(m_currentAccount.name())); m_precision = 2; } } } /** * This method clears the register, form, transaction list. See @sa m_register, * @sa m_transactionList */ void clear() { // clear current register contents m_register->clear(); // setup header font QFont font = KMyMoneySettings::listHeaderFontEx(); QFontMetrics fm(font); int height = fm.lineSpacing() + 6; m_register->horizontalHeader()->setMinimumHeight(height); m_register->horizontalHeader()->setMaximumHeight(height); m_register->horizontalHeader()->setFont(font); // setup cell font font = KMyMoneySettings::listCellFontEx(); m_register->setFont(font); // clear the form m_form->clear(); // the selected transactions list m_transactionList.clear(); // and the selected account in the combo box m_accountComboBox->setSelected(QString()); // fraction defaults to two digits m_precision = 2; } void loadView() { MYMONEYTRACER(tracer); Q_Q(KGlobalLedgerView); // setup form visibility m_formFrame->setVisible(KMyMoneySettings::transactionForm()); // no account selected // emit q->objectSelected(MyMoneyAccount()); // no transaction selected KMyMoneyRegister::SelectedTransactions list; emit q->selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); QMap isSelected; QString focusItemId; QString backUpFocusItemId; // in case the focus item is removed QString anchorItemId; QString backUpAnchorItemId; // in case the anchor item is removed if (!m_newAccountLoaded) { // remember the current selected transactions KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); for (; item; item = item->nextItem()) { if (item->isSelected()) { isSelected[item->id()] = true; } } // remember the item that has the focus storeId(m_register->focusItem(), focusItemId, backUpFocusItemId); // and the one that has the selection anchor storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId); } else { m_registerSearchLine->searchLine()->clear(); } // clear the current contents ... clear(); // ... load the combobox widget and select current account ... loadAccounts(); // ... setup the register columns ... m_register->setupRegister(m_currentAccount); // ... setup the form ... m_form->setupForm(m_currentAccount); if (m_currentAccount.id().isEmpty()) { // if we don't have an account we bail out q->setEnabled(false); return; } q->setEnabled(true); m_register->setUpdatesEnabled(false); // ... and recreate it KMyMoneyRegister::RegisterItem* focusItem = 0; KMyMoneyRegister::RegisterItem* anchorItem = 0; QMap actBalance, clearedBalance, futureBalance; QMap::iterator it_b; try { // setup the filter to select the transactions we want to display // and update the sort order QString sortOrder; QString key; QDate reconciliationDate = m_reconciliationDate; MyMoneyTransactionFilter filter(m_currentAccount.id()); // if it's an investment account, we also take care of // the sub-accounts (stock accounts) if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) filter.addAccount(m_currentAccount.accountList()); if (isReconciliationAccount()) { key = "kmm-sort-reconcile"; sortOrder = KMyMoneySettings::sortReconcileView(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } else { filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); key = "kmm-sort-std"; sortOrder = KMyMoneySettings::sortNormalView(); if (KMyMoneySettings::hideReconciledTransactions() && !m_currentAccount.isIncomeExpense()) { filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } } filter.setReportAllSplits(true); // check if we have an account override of the sort order if (!m_currentAccount.value(key).isEmpty()) sortOrder = m_currentAccount.value(key); // setup sort order m_register->setSortOrder(sortOrder); // retrieve the list from the engine MyMoneyFile::instance()->transactionList(m_transactionList, filter); emit q->slotStatusProgress(0, m_transactionList.count()); // create the elements for the register QList >::const_iterator it; QMapuniqueMap; int i = 0; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); actBalance[t->split().accountId()] = MyMoneyMoney(); emit q->slotStatusProgress(++i, 0); // if we're in reconciliation and the state is cleared, we // force the item to show in dimmed intensity to get a visual focus // on those items, that we need to work on if (isReconciliationAccount() && (*it).second.reconcileFlag() == eMyMoney::Split::State::Cleared) { t->setReducedIntensity(true); } } // create dummy entries for the scheduled transactions if sorted by postdate int period = KMyMoneySettings::schedulePreview(); if (m_register->primarySortKey() == eWidgets::SortField::PostDate) { // show scheduled transactions which have a scheduled postdate // within the next 'period' days. In reconciliation mode, the // period starts on the statement date. QDate endDate = QDate::currentDate().addDays(period); if (isReconciliationAccount()) endDate = reconciliationDate.addDays(period); QList scheduleList = MyMoneyFile::instance()->scheduleList(m_currentAccount.id()); while (!scheduleList.isEmpty()) { MyMoneySchedule& s = scheduleList.first(); for (;;) { if (s.isFinished() || s.adjustedNextDueDate() > endDate) { break; } MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s)); if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) { // if the transaction is scheduled and overdue, it can't // certainly be posted in the past. So we take today's date // as the alternative t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption())); } else { t.setPostDate(s.adjustedNextDueDate()); } foreach (const auto split, t.splits()) { if (split.accountId() == m_currentAccount.id()) { new KMyMoneyRegister::StdTransactionScheduled(m_register, t, split, uniqueMap[t.id()]); } } // keep track of this payment locally (not in the engine) if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) { s.setLastPayment(QDate::currentDate()); } else { s.setLastPayment(s.nextDueDate()); } // if this is a one time schedule, we can bail out here as we're done if (s.occurrence() == eMyMoney::Schedule::Occurrence::Once) break; // for all others, we check if the next payment date is still 'in range' QDate nextDueDate = s.nextPayment(s.nextDueDate()); if (nextDueDate.isValid()) { s.setNextDueDate(nextDueDate); } else { break; } } scheduleList.pop_front(); } } // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); // add special markers for reconciliation now so that they do not get // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting // of items but that's ok. KMyMoneyRegister::StatementGroupMarker* statement = 0; KMyMoneyRegister::StatementGroupMarker* dStatement = 0; KMyMoneyRegister::StatementGroupMarker* pStatement = 0; if (isReconciliationAccount()) { switch (m_register->primarySortKey()) { case eWidgets::SortField::PostDate: statement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Details")); m_register->sortItems(); break; case eWidgets::SortField::Type: dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Deposit Details")); pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Payment, reconciliationDate, i18n("Statement Payment Details")); m_register->sortItems(); break; default: break; } } // we need at least the balance for the account we currently show actBalance[m_currentAccount.id()] = MyMoneyMoney(); if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) foreach (const auto accountID, m_currentAccount.accountList()) actBalance[accountID] = MyMoneyMoney(); // determine balances (actual, cleared). We do this by getting the actual // balance of all entered transactions from the engine and walk the list // of transactions backward. Also re-select a transaction if it was // selected before and setup the focus item. MyMoneyMoney factor(1, 1); if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability || m_currentAccount.accountGroup() == eMyMoney::Account::Type::Equity) factor = -factor; QMap deposits; QMap payments; QMap depositAmount; QMap paymentAmount; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key()); balance = balance * factor; clearedBalance[it_b.key()] = futureBalance[it_b.key()] = (*it_b) = balance; deposits[it_b.key()] = payments[it_b.key()] = 0; depositAmount[it_b.key()] = MyMoneyMoney(); paymentAmount[it_b.key()] = MyMoneyMoney(); } tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); focusItem = 0; // take care of possibly trailing scheduled transactions (bump up the future balance) while (p) { if (p->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isScheduled()) { MyMoneyMoney balance = futureBalance[t->split().accountId()]; const MyMoneySplit& split = t->split(); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance = balance * split.shares(); } else { balance += split.shares() * factor; } futureBalance[split.accountId()] = balance; } else if (t && !focusItem) focusItem = p; } p = p->prevItem(); } p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (isSelected.contains(t->id())) t->setSelected(true); matchItemById(&focusItem, t, focusItemId, backUpFocusItemId); matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId); const MyMoneySplit& split = t->split(); MyMoneyMoney balance = futureBalance[split.accountId()]; t->setBalance(balance); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance /= split.shares(); } else { balance -= split.shares() * factor; } if (!t->isScheduled()) { if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == eMyMoney::Split::State::Cleared) { if (split.shares().isNegative()) { payments[split.accountId()]++; paymentAmount[split.accountId()] += split.shares(); } else { deposits[split.accountId()]++; depositAmount[split.accountId()] += split.shares(); } } if (t->transaction().postDate() > QDate::currentDate()) { tracer.printf("Reducing actual balance by %s because %s/%s(%s) is in the future", qPrintable((split.shares() * factor).formatMoney("", 2)), qPrintable(t->transaction().id()), qPrintable(split.id()), qPrintable(t->transaction().postDate().toString(Qt::ISODate))); actBalance[split.accountId()] -= split.shares() * factor; } } futureBalance[split.accountId()] = balance; } p = p->prevItem(); } clearedBalance[m_currentAccount.id()] = MyMoneyFile::instance()->clearedBalance(m_currentAccount.id(), reconciliationDate); tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); // update statement information if (statement) { const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()], paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2", aboutDeposits, aboutPayments)); } if (pStatement) { pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()] , paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } if (dStatement) { dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()] , depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } // add a last empty entry for new transactions // leave some information about the current account MyMoneySplit split; split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // make sure to use the value specified in the option during reconciliation if (isReconciliationAccount()) split.setReconcileFlag(static_cast(KMyMoneySettings::defaultReconciliationState())); - KMyMoneyRegister::Register::transactionFactory(m_register, MyMoneyTransaction(), split, 0); + MyMoneyTransaction emptyTransaction; + emptyTransaction.setCommodity(m_currentAccount.currencyId()); + KMyMoneyRegister::Register::transactionFactory(m_register, emptyTransaction, split, 0); m_register->updateRegister(true); if (focusItem) { // in case we have some selected items we just set the focus item // in other cases, we make the focusitem also the selected item if (anchorItem && (anchorItem != focusItem)) { m_register->setFocusItem(focusItem); m_register->setAnchorItem(anchorItem); } else m_register->selectItem(focusItem, true); } else { // just use the empty line at the end if nothing else exists in the ledger p = m_register->lastItem(); m_register->setFocusItem(p); m_register->selectItem(p); focusItem = p; } updateSummaryLine(actBalance, clearedBalance); emit q->slotStatusProgress(-1, -1); } catch (const MyMoneyException &) { m_currentAccount = MyMoneyAccount(); clear(); } m_showDetails = KMyMoneySettings::showRegisterDetailed(); // and tell everyone what's selected emit q->selectByObject(m_currentAccount, eView::Intent::None); KMyMoneyRegister::SelectedTransactions actualSelection(m_register); emit q->selectByVariant(QVariantList {QVariant::fromValue(actualSelection)}, eView::Intent::SelectRegisterTransactions); } void selectTransaction(const QString& id) { if (!id.isEmpty()) { KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->transaction().id() == id) { m_register->selectItem(t); m_register->ensureItemVisible(t); break; } } p = p->prevItem(); } } } /** * @brief selects transactions for processing with slots * @param list of transactions * @return false if only schedule is to be selected */ bool selectTransactions(const KMyMoneyRegister::SelectedTransactions list) { Q_Q(KGlobalLedgerView); // list can either contain a list of transactions or a single selected scheduled transaction // in the latter case, the transaction id is actually the one of the schedule. In order // to differentiate between the two, we just ask for the schedule. If we don't find one - because // we passed the id of a real transaction - then we know that fact. We use the schedule here, // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips // to the backend which we would have to do if we check for the transaction. m_selectedTransactions.clear(); auto sch = MyMoneySchedule(); auto ret = true; m_accountGoto.clear(); m_payeeGoto.clear(); if (!list.isEmpty() && !list.first().isScheduled()) { m_selectedTransactions = list; if (list.count() == 1) { const MyMoneySplit& sp = m_selectedTransactions[0].split(); if (!sp.payeeId().isEmpty()) { try { auto payee = MyMoneyFile::instance()->payee(sp.payeeId()); if (!payee.name().isEmpty()) { m_payeeGoto = payee.id(); auto name = payee.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToPayee]->setText(i18n("Go to '%1'", name)); } } catch (const MyMoneyException &) { } } try { const auto& t = m_selectedTransactions[0].transaction(); // search the first non-income/non-expense account and use it for the 'goto account' const auto& selectedTransactionSplit = m_selectedTransactions[0].split(); foreach (const auto split, t.splits()) { if (split.id() != selectedTransactionSplit.id()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.isIncomeExpense()) { // for stock accounts we show the portfolio account if (acc.isInvest()) { acc = MyMoneyFile::instance()->account(acc.parentAccountId()); } m_accountGoto = acc.id(); auto name = acc.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToAccount]->setText(i18n("Go to '%1'", name)); break; } } } } catch (const MyMoneyException &) { } } } else if (!list.isEmpty()) { sch = MyMoneyFile::instance()->schedule(list.first().scheduleId()); m_selectedTransactions.append(list.first()); ret = false; } emit q->selectByObject(sch, eView::Intent::None); // make sure, we show some neutral menu entry if we don't have an object if (m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setText(i18n("Go to payee")); if (m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setText(i18n("Go to account")); return ret; } /** * Returns @a true if setReconciliationAccount() has been called for * the current loaded account. * * @retval true current account is in reconciliation mode * @retval false current account is not in reconciliation mode */ bool isReconciliationAccount() const { return m_currentAccount.id() == m_reconciliationAccount.id(); } /** * Updates the values on the summary line beneath the register with * the given values. The contents shown differs between reconciliation * mode and normal mode. * * @param actBalance map of account indexed values to be used as actual balance * @param clearedBalance map of account indexed values to be used as cleared balance */ void updateSummaryLine(const QMap& actBalance, const QMap& clearedBalance) { Q_Q(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); m_leftSummaryLabel->show(); m_centerSummaryLabel->show(); m_rightSummaryLabel->show(); if (isReconciliationAccount()) { if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_leftSummaryLabel->setText(i18n("Statement: %1", m_endingBalance.formatMoney("", m_precision))); m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = clearedBalance[m_currentAccount.id()] - m_endingBalance; } } else { // update summary line in normal mode QDate reconcileDate = m_currentAccount.lastReconciliationDate(); if (reconcileDate.isValid()) { m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat))); } else { m_leftSummaryLabel->setText(i18n("Never reconciled")); } QPalette palette = m_rightSummaryLabel->palette(); palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(q->foregroundRole())); if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = actBalance[m_currentAccount.id()]; } else { m_centerSummaryLabel->hide(); MyMoneyMoney balance; MyMoneySecurity base = file->baseCurrency(); QMap::const_iterator it_b; // reset the approximated flag m_balanceIsApproximated = false; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyAccount stock = file->account(it_b.key()); QString currencyId = stock.currencyId(); MyMoneySecurity sec = file->security(currencyId); MyMoneyMoney rate(1, 1); if (stock.isInvest()) { currencyId = sec.tradingCurrency(); const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId); m_balanceIsApproximated |= !priceInfo.isValid(); rate = priceInfo.rate(sec.tradingCurrency()); } if (currencyId != base.id()) { const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id()); m_balanceIsApproximated |= !priceInfo.isValid(); rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision()); } balance += ((*it_b) * rate).convert(base.smallestAccountFraction()); } m_totalBalance = balance; } m_rightSummaryLabel->setPalette(palette); } // determine the number of selected transactions KMyMoneyRegister::SelectedTransactions selection; m_register->selectedTransactions(selection); q->slotUpdateSummaryLine(selection); } /** * setup the default action according to the current account type */ void setupDefaultAction() { switch (m_currentAccount.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Savings: m_action = eWidgets::eRegister::Action::Deposit; break; default: m_action = eWidgets::eRegister::Action::Withdrawal; break; } } // used to store the id of an item and the id of an immediate unselected sibling void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) { if (item) { // the id of the item id = item->id(); // the id of the item's previous/next unselected item for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) { if (!it->isSelected()) { backupId = it->id(); } } // if we didn't found previous unselected items search trough the next items for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) { if (!it->isSelected()) { backupId = it->id(); } } } } // use to match an item by it's id or a backup id which has a lower precedence void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) { if (!backupId.isEmpty() && t->id() == backupId) *item = t; if (!id.isEmpty() && t->id() == id) { // we found the real thing there's no need for the backup anymore backupId.clear(); *item = t; } } bool canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { if (m_register->focusItem() == 0) return false; bool rc = true; if (list.warnLevel() == KMyMoneyRegister::SelectedTransaction::OneAccountClosed) { // scan all splits for the first closed account QString closedAccount; foreach(const auto selectedTransaction, list) { foreach(const auto split, selectedTransaction.transaction().splits()) { const auto id = split.accountId(); const auto acc = MyMoneyFile::instance()->account(id); if (acc.isClosed()) { closedAccount = acc.name(); // we're done rc = false; break; } } if(!rc) break; } tooltip = i18n("Cannot process transactions in account %1, which is closed.", closedAccount); showTooltip(tooltip); return false; } if (!m_register->focusItem()->isSelected()) { tooltip = i18n("Cannot process transaction with focus if it is not selected."); showTooltip(tooltip); return false; } tooltip.clear(); return !list.isEmpty(); } void showTooltip(const QString msg) const { QToolTip::showText(m_tooltipPosn, msg); } bool createNewTransaction() { Q_Q(KGlobalLedgerView); auto rc = false; QString txt; if (q->canCreateTransactions(txt)) { rc = q->selectEmptyTransaction(); } return rc; } TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_Q(KGlobalLedgerView); TransactionEditor* editor = 0; QString txt; if (q->canEditTransactions(list, txt) || q->canCreateTransactions(txt)) { editor = q->startEdit(list); } return editor; } void doDeleteTransactions() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int cnt = list.count(); int i = 0; emit q->slotStatusProgress(0, cnt); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); try { it_t = list.begin(); while (it_t != list.end()) { // only remove those transactions that do not reference a closed account if (!file->referencesClosedAccount((*it_t).transaction())) { file->removeTransaction((*it_t).transaction()); // remove all those references in the list of selected transactions // that refer to the same transaction we just removed so that we // will not be caught by an exception later on (see bko #285310) KMyMoneyRegister::SelectedTransactions::iterator it_td = it_t; ++it_td; while (it_td != list.end()) { if ((*it_t).transaction().id() == (*it_td).transaction().id()) { it_td = list.erase(it_td); i++; // bump count of deleted transactions } else { ++it_td; } } } // need to ensure "nextCheckNumber" is still correct auto acc = file->account((*it_t).split().accountId()); // the "lastNumberUsed" might have been the txn number deleted // so adjust it QString deletedNum = (*it_t).split().number(); if (deletedNum == acc.value("lastNumberUsed")) { // decrement deletedNum and set new "lastNumberUsed" QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); acc.setValue("lastNumberUsed", num); file->modifyAccount(acc); } list.erase(it_t); it_t = list.begin(); emit q->slotStatusProgress(i++, 0); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to delete transaction(s)"), e.what()); } emit q->slotStatusProgress(-1, -1); } void deleteTransactionEditor() { // make sure, we don't use the transaction editor pointer // anymore from now on auto p = m_transactionEditor; m_transactionEditor = nullptr; delete p; } void transactionUnmatch() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions::const_iterator it; MyMoneyFileTransaction ft; try { for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).split().isMatched()) { TransactionMatcher matcher(m_currentAccount); matcher.unmatch((*it).transaction(), (*it).split()); } } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to unmatch the selected transactions"), e.what()); } } void transactionMatch() { Q_Q(KGlobalLedgerView); if (m_selectedTransactions.count() != 2) return; MyMoneyTransaction startMatchTransaction; MyMoneyTransaction endMatchTransaction; MyMoneySplit startSplit; MyMoneySplit endSplit; KMyMoneyRegister::SelectedTransactions::const_iterator it; KMyMoneyRegister::SelectedTransactions toBeDeleted; for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) { if (endMatchTransaction.id().isEmpty()) { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } else { //This is a second imported transaction, we still want to merge startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } } else if (!(*it).split().isMatched()) { if (startMatchTransaction.id().isEmpty()) { startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } else { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } } } #if 0 KMergeTransactionsDlg dlg(m_selectedAccount); dlg.addTransaction(startMatchTransaction); dlg.addTransaction(endMatchTransaction); if (dlg.exec() == QDialog::Accepted) #endif { MyMoneyFileTransaction ft; try { if (startMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No manually entered transaction selected for matching")); if (endMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No imported transaction selected for matching")); TransactionMatcher matcher(m_currentAccount); matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to match the selected transactions"), e.what()); } } } /** * Mark the selected transactions as provided by @a flag. If * flag is @a MyMoneySplit::Unknown, the future state depends * on the current stat of the split's flag according to the * following table: * * - NotReconciled --> Cleared * - Cleared --> Reconciled * - Reconciled --> NotReconciled */ void markTransaction(eMyMoney::Split::State flag) { Q_Q(KGlobalLedgerView); auto list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; auto cnt = list.count(); auto i = 0; emit q->slotStatusProgress(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // turn on signals before we modify the last entry in the list cnt--; MyMoneyFile::instance()->blockSignals(cnt != 0); // get a fresh copy auto t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); auto sp = t.splitById((*it_t).split().id()); if (sp.reconcileFlag() != flag) { if (flag == eMyMoney::Split::State::Unknown) { if (m_reconciliationAccount.id().isEmpty()) { // in normal mode we cycle through all states switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; case eMyMoney::Split::State::Reconciled: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } else { // in reconciliation mode we skip the reconciled state switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } } else { sp.setReconcileFlag(flag); } t.modifySplit(sp); MyMoneyFile::instance()->modifyTransaction(t); } emit q->slotStatusProgress(i++, 0); } emit q->slotStatusProgress(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to modify transaction"), e.what()); } } // move a stock transaction from one investment account to another void moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void createTransactionMoveMenu() { Q_Q(KGlobalLedgerView); if (!m_moveToAccountSelector) { auto menu = pMenus[eMenu::Menu::MoveTransaction]; if (menu ) { auto accountSelectorAction = new QWidgetAction(menu); m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false); m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); accountSelectorAction->setDefaultWidget(m_moveToAccountSelector); menu->addAction(accountSelectorAction); q->connect(m_moveToAccountSelector, &QObject::destroyed, q, &KGlobalLedgerView::slotObjectDestroyed); q->connect(m_moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &KGlobalLedgerView::slotMoveToAccount); } } } QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { Q_Q(KGlobalLedgerView); static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; // KMSTATUS(i18n("Running automatic reconciliation")); auto progressBarIndex = 0; q->slotStatusProgress(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } q->slotStatusProgress(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } q->slotStatusProgress(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const auto& security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > it_ts(transactions); while (it_ts.hasNext()) { const QPair &transactionSplit = it_ts.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { q->slotStatusProgress(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } q->slotStatusProgress(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif q->slotStatusProgress(-1, -1); return result; } KGlobalLedgerView *q_ptr; MousePressFilter *m_mousePressFilter; KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine; // QString m_reconciliationAccount; QDate m_reconciliationDate; MyMoneyMoney m_endingBalance; int m_precision; bool m_recursion; bool m_showDetails; eWidgets::eRegister::Action m_action; // models AccountNamesFilterProxyModel *m_filterProxyModel; // widgets KMyMoneyAccountCombo* m_accountComboBox; MyMoneyMoney m_totalBalance; bool m_balanceIsApproximated; // frames QFrame* m_toolbarFrame; QFrame* m_registerFrame; QFrame* m_buttonFrame; QFrame* m_formFrame; QFrame* m_summaryFrame; // widgets KMyMoneyRegister::Register* m_register; KToolBar* m_buttonbar; /** * This member holds the currently selected account */ MyMoneyAccount m_currentAccount; QString m_lastSelectedAccountID; MyMoneyAccount m_reconciliationAccount; /** * This member holds the transaction list */ QList > m_transactionList; QLabel* m_leftSummaryLabel; QLabel* m_centerSummaryLabel; QLabel* m_rightSummaryLabel; KMyMoneyTransactionForm::TransactionForm* m_form; /** * This member holds the load state of page */ bool m_needLoad; bool m_newAccountLoaded; bool m_inEditMode; QWidgetList m_tabOrderWidgets; QPoint m_tooltipPosn; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; /** * This member keeps the date that was used as the last posting date. * It will be updated whenever the user modifies the post date * and is used to preset the posting date when new transactions are created. * This member is initialised to the current date when the program is started. */ static QDate m_lastPostDate; // pointer to the current transaction editor QPointer m_transactionEditor; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QString m_lastPayeeEnteredId; QScopedPointer m_balanceWarning; KMyMoneyAccountSelector* m_moveToAccountSelector; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; KFindTransactionDlg* m_searchDlg; }; #endif diff --git a/kmymoney/views/khomeview.cpp b/kmymoney/views/khomeview.cpp index 4256f77dd..d6ad25753 100644 --- a/kmymoney/views/khomeview.cpp +++ b/kmymoney/views/khomeview.cpp @@ -1,211 +1,221 @@ /*************************************************************************** khomeview.cpp - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "khomeview_p.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes KHomeView::KHomeView(QWidget *parent) : KMyMoneyViewBase(*new KHomeViewPrivate(this), parent) { } KHomeView::~KHomeView() { } +void KHomeView::slotAdjustScrollPos() +{ +#ifndef ENABLE_WEBENGINE + Q_D(KHomeView); + if (d && d->m_view && d->m_view->page() && d->m_view->page()->mainFrame()) { + d->m_view->page()->mainFrame()->setScrollBarValue(Qt::Vertical, d->m_scrollBarPos); + } +#endif +} + void KHomeView::wheelEvent(QWheelEvent* event) { Q_D(KHomeView); // Zoom text on Ctrl + Scroll if (event->modifiers() & Qt::CTRL) { qreal factor = d->m_view->zoomFactor(); if (event->delta() > 0) factor += 0.1; else if (event->delta() < 0) factor -= 0.1; d->m_view->setZoomFactor(factor); event->accept(); return; } } void KHomeView::executeCustomAction(eView::Action action) { Q_D(KHomeView); switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::Print: slotPrintView(); break; case eView::Action::CleanupBeforeFileClose: d->m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); break; default: break; } } void KHomeView::refresh() { Q_D(KHomeView); if (isVisible()) { d->loadView(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KHomeView::showEvent(QShowEvent* event) { Q_D(KHomeView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Home, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); QWidget::showEvent(event); } void KHomeView::slotPrintView() { Q_D(KHomeView); if (d->m_view) { d->m_currentPrinter = new QPrinter(); QPointer dialog = new QPrintDialog(d->m_currentPrinter, this); dialog->setWindowTitle(QString()); if (dialog->exec() != QDialog::Accepted) { delete d->m_currentPrinter; d->m_currentPrinter = nullptr; return; } - #ifdef ENABLE_WEBENGINE + #ifdef ENABLE_WEBENGINE d->m_view->page()->print(d->m_currentPrinter, [=] (bool) {delete d->m_currentPrinter; d->m_currentPrinter = nullptr;}); - #else - d->m_view->print(d->m_currentPrinter); - #endif + #else + d->m_view->print(d->m_currentPrinter); + #endif } } void KHomeView::slotOpenUrl(const QUrl &url) { Q_D(KHomeView); QString protocol = url.scheme(); QString view = url.fileName(); // empty view -> bail out if (view.isEmpty()) return; QUrlQuery query(url); QString id = query.queryItemValue("id"); QString mode = query.queryItemValue("mode"); const auto file = MyMoneyFile::instance(); if (protocol == QLatin1String("https")) { QDesktopServices::openUrl(url); } else if (protocol == QLatin1String("mailto")) { QDesktopServices::openUrl(url); } else { KXmlGuiWindow* mw = KMyMoneyUtils::mainWindow(); Q_CHECK_PTR(mw); if (view == VIEW_LEDGER) { emit selectByVariant(QVariantList {QVariant(id), QVariant(QString())}, eView::Intent::ShowTransaction); } else if (view == VIEW_SCHEDULE) { if (mode == QLatin1String("enter")) { emit selectByObject(file->schedule(id), eView::Intent::None); QTimer::singleShot(0, pActions[eMenu::Action::EnterSchedule], SLOT(trigger())); } else if (mode == QLatin1String("edit")) { emit selectByObject(file->schedule(id), eView::Intent::None); QTimer::singleShot(0, pActions[eMenu::Action::EditSchedule], SLOT(trigger())); } else if (mode == QLatin1String("skip")) { emit selectByObject(file->schedule(id), eView::Intent::None); QTimer::singleShot(0, pActions[eMenu::Action::SkipSchedule], SLOT(trigger())); } else if (mode == QLatin1String("full")) { d->m_showAllSchedules = true; d->loadView(); } else if (mode == QLatin1String("reduced")) { d->m_showAllSchedules = false; d->loadView(); } } else if (view == VIEW_REPORTS) { emit selectByObject(file->report(id), eView::Intent::OpenObject); // emit openObjectRequested(file->report(id)); } else if (view == VIEW_WELCOME) { if (mode == QLatin1String("whatsnew")) d->m_view->setHtml(KWelcomePage::whatsNewPage(), QUrl("file://")); else d->m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else if (view == QLatin1String("action")) { QTimer::singleShot(0, mw->actionCollection()->action(id), SLOT(trigger())); } else if (view == VIEW_HOME) { QList list; // it could be, that we don't even have a storage object attached. // in this case the call to accountList() will throw an MyMoneyException // which we catch here and treat it as 'no accounts found'. try { MyMoneyFile::instance()->accountList(list); } catch(const MyMoneyException& e) { } if (list.count() == 0) { KMessageBox::information(this, i18n("Before KMyMoney can give you detailed information about your financial status, you need to create at least one account. Until then, KMyMoney shows the welcome page instead.")); } d->loadView(); } else { qDebug("Unknown view '%s' in slotOpenURL()", qPrintable(view)); } } } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef VIEW_LEDGER #undef VIEW_SCHEDULE #undef VIEW_WELCOME #undef VIEW_HOME #undef VIEW_REPORTS diff --git a/kmymoney/views/khomeview.h b/kmymoney/views/khomeview.h index eb4a44387..c1445de55 100644 --- a/kmymoney/views/khomeview.h +++ b/kmymoney/views/khomeview.h @@ -1,74 +1,75 @@ /*************************************************************************** khomeview.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KHOMEVIEW_H #define KHOMEVIEW_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase.h" /** * Displays a 'home page' for the user. Similar to concepts used in * quicken and m$-money. * * @author Michael Edwardes * * @short A view containing the home page for kmymoney. **/ class KHomeViewPrivate; class KHomeView : public KMyMoneyViewBase { Q_OBJECT public: explicit KHomeView(QWidget *parent = nullptr); ~KHomeView() override; void executeCustomAction(eView::Action action) override; void refresh(); protected: void showEvent(QShowEvent* event) override; void wheelEvent(QWheelEvent *event) override; public Q_SLOTS: /** * Print the current view */ void slotPrintView(); private: Q_DECLARE_PRIVATE(KHomeView) private Q_SLOTS: void slotOpenUrl(const QUrl &url); + void slotAdjustScrollPos(); }; #endif diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index 080cde8b9..fef8682eb 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,1857 +1,1855 @@ /*************************************************************************** khomeview_p.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KHOMEVIEW_P_H #define KHOMEVIEW_P_H #include "khomeview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "mymoneyutils.h" #include "kmymoneyutils.h" #include "kwelcomepage.h" #include "kmymoneysettings.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" #include "kmymoneyplugin.h" #include "mymoneyenums.h" #include "menuenums.h" #include "plugins/views/reports/reportsviewenums.h" #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" using namespace Icons; using namespace eMyMoney; /** * @brief Converts a QPixmap to an data URI scheme * * According to RFC 2397 * * @param pixmap Source to convert * @return full data URI */ QString QPixmapToDataUri(const QPixmap& pixmap) { QImage image(pixmap.toImage()); QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer return QLatin1String("data:image/png;base64,") + QString(byteArray.toBase64()); } bool accountNameLess(const MyMoneyAccount &acc1, const MyMoneyAccount &acc2) { return acc1.name().localeAwareCompare(acc2.name()) < 0; } class KHomeViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KHomeView) public: explicit KHomeViewPrivate(KHomeView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), m_view(nullptr), m_showAllSchedules(false), m_needLoad(true), m_netWorthGraphLastValidSize(400, 300), m_currentPrinter(nullptr) { } ~KHomeViewPrivate() { // if user wants to remember the font size, store it here if (KMyMoneySettings::rememberZoomFactor() && m_view) { KMyMoneySettings::setZoomFactor(m_view->zoomFactor()); KMyMoneySettings::self()->save(); } } /** * Definition of bitmap used as argument for showAccounts(). */ enum paymentTypeE { Preferred = 1, ///< show preferred accounts Payment = 2 ///< show payment accounts }; void init() { Q_Q(KHomeView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); #ifdef ENABLE_WEBENGINE m_view = new QWebEngineView(q); #else m_view = new KWebView(q); #endif m_view->setPage(new MyQWebEnginePage(m_view)); vbox->addWidget(m_view); #ifdef ENABLE_WEBENGINE q->connect(m_view->page(), &QWebEnginePage::urlChanged, q, &KHomeView::slotOpenUrl); #else m_view->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); q->connect(m_view->page(), &KWebPage::linkClicked, q, &KHomeView::slotOpenUrl); #endif q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KHomeView::refresh); } /** * Print an account and its balance and limit */ void showAccountEntry(const MyMoneyAccount& acc, const MyMoneyMoney& value, const MyMoneyMoney& valueToMinBal, const bool showMinBal) { MyMoneyFile* file = MyMoneyFile::instance(); QString tmp; MyMoneySecurity currency = file->currency(acc.currencyId()); QString amount; QString amountToMinBal; //format amounts amount = MyMoneyUtils::formatMoney(value, acc, currency); amount.replace(QChar(' '), " "); if (showMinBal) { amountToMinBal = MyMoneyUtils::formatMoney(valueToMinBal, acc, currency); amountToMinBal.replace(QChar(' '), " "); } QString cellStatus, cellCounts, pathOK, pathTODO, pathNotOK; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { //show account's online-status pathOK = QPixmapToDataUri(Icons::get(Icon::DialogOKApply).pixmap(QSize(16,16))); pathTODO = QPixmapToDataUri(Icons::get(Icon::MailReceive).pixmap(QSize(16,16))); pathNotOK = QPixmapToDataUri(Icons::get(Icon::DialogCancel).pixmap(QSize(16,16))); if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) cellStatus = '-'; else if (file->hasMatchingOnlineBalance(acc)) { if (file->hasNewerTransaction(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate))) cellStatus = QString("").arg(pathTODO); else cellStatus = QString("").arg(pathOK); } else cellStatus = QString("").arg(pathNotOK); tmp = QString("%1").arg(cellStatus); } tmp += QString("") + link(VIEW_LEDGER, QString("?id=%1").arg(acc.id())) + acc.name() + linkend() + ""; int countNotMarked = 0, countCleared = 0, countNotReconciled = 0; QString countStr; if (KMyMoneySettings::showCountOfUnmarkedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotMarked = m_transactionStats[acc.id()][(int)Split::State::NotReconciled]; if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = m_transactionStats[acc.id()][(int)Split::State::Cleared]; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotReconciled = countNotMarked + countCleared; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) { if (countNotMarked) countStr = QString("%1").arg(countNotMarked); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfClearedTransactions()) { if (countCleared) countStr = QString("%1").arg(countCleared); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfNotReconciledTransactions()) { if (countNotReconciled) countStr = QString("%1").arg(countNotReconciled); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showDateOfLastReconciliation()) { const auto lastReconciliationDate = acc.lastReconciliationDate().toString(Qt::SystemLocaleShortDate).replace(QChar(' '), " "); tmp += QString("%1").arg(lastReconciliationDate); } //show account balance tmp += QString("%1").arg(showColoredAmount(amount, value.isNegative())); //show minimum balance column if requested if (showMinBal) { //if it is an investment, show minimum balance empty if (acc.accountType() == Account::Type::Investment) { tmp += QString(" "); } else { //show minimum balance entry tmp += QString("%1").arg(showColoredAmount(amountToMinBal, valueToMinBal.isNegative())); } } // qDebug("accountEntry = '%s'", tmp.toLatin1()); m_html += tmp; } void showAccountEntry(const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); MyMoneyMoney value; bool showLimit = KMyMoneySettings::showLimitInfo(); if (acc.accountType() == Account::Type::Investment) { //investment accounts show the balances of all its subaccounts value = investmentBalance(acc); //investment accounts have no minimum balance showAccountEntry(acc, value, MyMoneyMoney(), showLimit); } else { //get balance for normal accounts value = file->balance(acc.id(), QDate::currentDate()); if (acc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(acc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(file->baseCurrency().smallestAccountFraction()); m_total += baseValue; } else { m_total += value; } //if credit card or checkings account, show maximum credit if (acc.accountType() == Account::Type::CreditCard || acc.accountType() == Account::Type::Checkings) { QString maximumCredit = acc.value("maxCreditAbsolute"); if (maximumCredit.isEmpty()) { maximumCredit = acc.value("minBalanceAbsolute"); } MyMoneyMoney maxCredit = MyMoneyMoney(maximumCredit); showAccountEntry(acc, value, value - maxCredit, showLimit); } else { //otherwise use minimum balance QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); showAccountEntry(acc, value, value - minBalance, showLimit); } } } /** * @param acc the investment account * @return the balance in the currency of the investment account */ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto value = file->balance(acc.id(), QDate::currentDate()); foreach (const auto accountID, acc.accountList()) { auto stock = file->account(accountID); if (!stock.isClosed()) { try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id(), QDate::currentDate()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = (balance * price.rate(security.tradingCurrency())).convertPrecision(security.pricePrecision()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } } return value; } /** * Print text in the color set for negative numbers, if @p amount is negative * abd @p isNegative is true */ QString showColoredAmount(const QString& amount, bool isNegative) { if (isNegative) { //if negative, get the settings for negative numbers return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); } //if positive, return the same string return amount; } /** * Run the forecast */ void doForecast() { //clear m_accountList because forecast is about to changed m_accountList.clear(); //reinitialize the object m_forecast = KMyMoneyUtils::forecast(); //If forecastDays lower than accountsCycle, adjust to the first cycle if (m_forecast.accountsCycle() > m_forecast.forecastDays()) m_forecast.setForecastDays(m_forecast.accountsCycle()); //Get all accounts of the right type to calculate forecast m_forecast.doForecast(); } /** * Calculate the forecast balance after a payment has been made */ MyMoneyMoney forecastPaymentBalance(const MyMoneyAccount& acc, const MyMoneyMoney& payment, QDate& paymentDate) { //if paymentDate before or equal to currentDate set it to current date plus 1 //so we get to accumulate forecast balance correctly if (paymentDate <= QDate::currentDate()) paymentDate = QDate::currentDate().addDays(1); //check if the account is already there if (m_accountList.find(acc.id()) == m_accountList.end() || m_accountList[acc.id()].find(paymentDate) == m_accountList[acc.id()].end()) { if (paymentDate == QDate::currentDate()) { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate); } else { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate.addDays(-1)); } } m_accountList[acc.id()][paymentDate] = m_accountList[acc.id()][paymentDate] + payment; return m_accountList[acc.id()][paymentDate]; } void loadView() { + Q_Q(KHomeView); m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; if (MyMoneyFile::instance()->storage()) { MyMoneyFile::instance()->accountList(list); } if (list.isEmpty()) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { // preload transaction statistics m_transactionStats = MyMoneyFile::instance()->countTransactionsWithSpecificReconciliationState(); // keep current location on page - int scrollBarPos = 0; -#ifdef ENABLE_WEBENGINE - /// @todo cannot test this -#else - scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); + m_scrollBarPos = 0; +#ifndef ENABLE_WEBENGINE + m_scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); #endif //clear the forecast flag so it will be reloaded m_forecast.setForecastDone(false); const QString filename = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css"); QString header = QString("\n\n").arg(QUrl::fromLocalFile(filename).url()); header += KMyMoneyUtils::variableCSS(); header += "\n"; QString footer = "\n"; m_html.clear(); m_html += header; m_html += QString("
%1
").arg(i18n("Your Financial Summary")); QStringList settings = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; for (it = settings.constBegin(); it != settings.constEnd(); ++it) { int option = (*it).toInt(); if (option > 0) { switch (option) { case 1: // payments showPayments(); break; case 2: // preferred accounts showAccounts(Preferred, i18n("Preferred Accounts")); break; case 3: // payment accounts // Check if preferred accounts are shown separately if (settings.contains("2")) { showAccounts(static_cast(Payment | Preferred), i18n("Payment Accounts")); } else { showAccounts(Payment, i18n("Payment Accounts")); } break; case 4: // favorite reports showFavoriteReports(); break; case 5: // forecast showForecast(); break; case 6: // net worth graph over all accounts showNetWorthGraph(); break; case 8: // assets and liabilities showAssetsLiabilities(); break; case 9: // budget showBudget(); break; case 10: // cash flow summary showCashFlowSummary(); break; } m_html += "
 
\n"; } } m_html += "
"; m_html += link(VIEW_WELCOME, QString()) + i18n("Show KMyMoney welcome page") + linkend(); m_html += "
"; m_html += "
"; m_html += footer; m_view->setHtml(m_html, QUrl("file://")); - if (scrollBarPos) { -#ifdef ENABLE_WEBENGINE - /// @todo cannot test this -#else - m_view->page()->mainFrame()->setScrollBarValue(Qt::Vertical, scrollBarPos); -#endif +#ifndef ENABLE_WEBENGINE + if (m_scrollBarPos) { + QMetaObject::invokeMethod(q, "slotAdjustScrollPos", Qt::QueuedConnection); } +#endif } } void showNetWorthGraph() { Q_Q(KHomeView); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); m_netWorthGraphLastValidSize = netWorthGraphSize; m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); m_html += QString(""); m_html += QString(""); if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::NetWorthForecast); if (!variantReport.isNull()) { auto report = variantReport.value(); report->resize(m_netWorthGraphLastValidSize); m_html += QString("").arg(QPixmapToDataUri(report->grab())); delete report; } } else { m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); } m_html += QString(""); m_html += QString("
\"Networth\"
%1
"); } void showPayments() { MyMoneyFile* file = MyMoneyFile::instance(); QList overdues; QList schedule; int i = 0; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate::currentDate(), QDate::currentDate().addMonths(1), false); overdues = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedule.empty() && overdues.empty()) return; // HACK // Remove the finished schedules QList::Iterator d_it; //regular schedules d_it = schedule.begin(); while (d_it != schedule.end()) { if ((*d_it).isFinished()) { d_it = schedule.erase(d_it); continue; } ++d_it; } //overdue schedules d_it = overdues.begin(); while (d_it != overdues.end()) { if ((*d_it).isFinished()) { d_it = overdues.erase(d_it); continue; } ++d_it; } m_html += "
"; m_html += QString("
%1
\n").arg(i18n("Payments")); if (!overdues.isEmpty()) { m_html += "
 
\n"; qSort(overdues); QList::Iterator it; QList::Iterator it_f; m_html += ""; m_html += QString("\n").arg(showColoredAmount(i18n("Overdue payments"), true)); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (it = overdues.begin(); it != overdues.end(); ++it) { // determine number of overdue payments int cnt = (*it).transactionsRemainingUntil(QDate::currentDate().addDays(-1)); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it, cnt); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { qSort(schedule); // Extract todays payments if any QList todays; QList::Iterator t_it; for (t_it = schedule.begin(); t_it != schedule.end();) { if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { todays.append(*t_it); (*t_it).setNextDueDate((*t_it).nextPayment(QDate::currentDate())); // if adjustedNextDueDate is still currentDate then remove it from // scheduled payments if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { t_it = schedule.erase(t_it); continue; } } ++t_it; } if (todays.count() > 0) { m_html += "
 
\n"; m_html += ""; m_html += QString("\n").arg(i18n("Today's due payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (t_it = todays.begin(); t_it != todays.end(); ++t_it) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*t_it); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { m_html += "
 
\n"; QList::Iterator it; m_html += ""; m_html += QString("\n").arg(i18n("Future payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; // show all or the first 6 entries int cnt; cnt = (m_showAllSchedules) ? -1 : 6; bool needMoreLess = m_showAllSchedules; QDate lastDate = QDate::currentDate().addMonths(1); qSort(schedule); do { it = schedule.begin(); if (it == schedule.end()) break; // if the next due date is invalid (schedule is finished) // we remove it from the list QDate nextDate = (*it).nextDueDate(); if (!nextDate.isValid()) { schedule.erase(it); continue; } if (nextDate > lastDate) break; if (cnt == 0) { needMoreLess = true; break; } // in case we've shown the current recurrence as overdue, // we don't show it here again, but keep the schedule // as it might show up later in the list again if (!(*it).isOverdue()) { if (cnt > 0) --cnt; m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it); m_html += ""; // for single occurrence we have reported everything so we // better get out of here. if ((*it).occurrence() == Schedule::Occurrence::Once) { schedule.erase(it); continue; } } // if nextPayment returns an invalid date, setNextDueDate will // just skip it, resulting in a loop // we check the resulting date and erase the schedule if invalid if (!((*it).nextPayment((*it).nextDueDate())).isValid()) { schedule.erase(it); continue; } (*it).setNextDueDate((*it).nextPayment((*it).nextDueDate())); qSort(schedule); } while (1); if (needMoreLess) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += ""; m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; if (m_showAllSchedules) { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("reduced")) + i18nc("Less...", "Show fewer schedules on the list") + linkend(); } else { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("full")) + i18nc("More...", "Show more schedules on the list") + linkend(); } m_html += "
"; } } m_html += "
"; } void showPaymentEntry(const MyMoneySchedule& sched, int cnt = 1) { QString tmp; MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = sched.account(); if (!acc.id().isEmpty()) { MyMoneyTransaction t = sched.transaction(); // only show the entry, if it is still active if (!sched.isFinished()) { MyMoneySplit sp = t.splitByAccount(acc.id(), true); QString pathEnter = QPixmapToDataUri(Icons::get(Icon::KeyEnter).pixmap(QSize(16,16))); QString pathSkip = QPixmapToDataUri(Icons::get(Icon::MediaSkipForward).pixmap(QSize(16,16))); //show payment date tmp = QString("") + QLocale().toString(sched.adjustedNextDueDate(), QLocale::ShortFormat) + ""; if (!pathEnter.isEmpty()) tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=enter").arg(sched.id()), i18n("Enter schedule")) + QString("").arg(pathEnter) + linkend(); if (!pathSkip.isEmpty()) tmp += " " + link(VIEW_SCHEDULE, QString("?id=%1&mode=skip").arg(sched.id()), i18n("Skip schedule")) + QString("").arg(pathSkip) + linkend(); tmp += QString(" "); tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=edit").arg(sched.id()), i18n("Edit schedule")) + sched.name() + linkend(); //show quantity of payments overdue if any if (cnt > 1) tmp += i18np(" (%1 payment)", " (%1 payments)", cnt); //show account of the main split tmp += ""; tmp += QString(file->account(acc.id()).name()); //show amount of the schedule tmp += ""; const MyMoneySecurity& currency = MyMoneyFile::instance()->currency(acc.currencyId()); MyMoneyMoney payment = MyMoneyMoney(sp.value(t.commodity(), acc.currencyId()) * cnt); QString amount = MyMoneyUtils::formatMoney(payment, acc, currency); amount.replace(QChar(' '), " "); tmp += showColoredAmount(amount, payment.isNegative()); tmp += ""; //show balance after payments tmp += ""; QDate paymentDate = QDate(sched.adjustedNextDueDate()); MyMoneyMoney balanceAfter = forecastPaymentBalance(acc, payment, paymentDate); QString balance = MyMoneyUtils::formatMoney(balanceAfter, acc, currency); balance.replace(QChar(' '), " "); tmp += showColoredAmount(balance, balanceAfter.isNegative()); tmp += ""; // qDebug("paymentEntry = '%s'", tmp.toLatin1()); m_html += tmp; } } } catch (const MyMoneyException &e) { qDebug("Unable to display schedule entry: %s", e.what()); } } void showAccounts(paymentTypeE type, const QString& header) { MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QList accounts; auto showClosedAccounts = KMyMoneySettings::showAllAccounts(); // get list of all accounts file->accountList(accounts); for (QList::Iterator it = accounts.begin(); it != accounts.end();) { bool removeAccount = false; if (!(*it).isClosed() || showClosedAccounts) { switch ((*it).accountType()) { case Account::Type::Expense: case Account::Type::Income: // never show a category account // Note: This might be different in a future version when // the homepage also shows category based information removeAccount = true; break; // Asset and Liability accounts are only shown if they // have the preferred flag set case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Investment: // if preferred accounts are requested, then keep in list if ((*it).value("PreferredAccount") != "Yes" || (type & Preferred) == 0) { removeAccount = true; } break; // Check payment accounts. If payment and preferred is selected, // then always show them. If only payment is selected, then // show only if preferred flag is not set. case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: switch (type & (Payment | Preferred)) { case Payment: if ((*it).value("PreferredAccount") == "Yes") removeAccount = true; break; case Preferred: if ((*it).value("PreferredAccount") != "Yes") removeAccount = true; break; case Payment | Preferred: break; default: removeAccount = true; break; } break; // filter all accounts that are not used on homepage views default: removeAccount = true; break; } } else if ((*it).isClosed() || (*it).isInvest()) { // don't show if closed or a stock account removeAccount = true; } if (removeAccount) it = accounts.erase(it); else ++it; } if (!accounts.isEmpty()) { // sort the accounts by name qStableSort(accounts.begin(), accounts.end(), accountNameLess); QString tmp; int i = 0; tmp = "
" + header + "
\n
 
\n"; m_html += tmp; m_html += ""; m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::Download).pixmap(QSize(16,16))); m_html += QString("").arg(pathStatusHeader); } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += QString(""); if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += QString("").arg(i18n("Last Reconciled")); m_html += ""; //only show limit info if user chose to do so if (KMyMoneySettings::showLimitInfo()) { m_html += ""; } m_html += ""; m_total = 0; QList::const_iterator it_m; for (it_m = accounts.constBegin(); it_m != accounts.constEnd(); ++it_m) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showAccountEntry(*it_m); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); QString amount = m_total.formatMoney(file->baseCurrency().tradingSymbol(), prec); if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) m_html += ""; m_html += QString("").arg(i18n("Total")); if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += QString("").arg(showColoredAmount(amount, m_total.isNegative())); m_html += "
"; m_html += i18n("Account"); m_html += "!MC!R%1"; m_html += i18n("Current Balance"); m_html += ""; m_html += i18n("To Minimum Balance / Maximum Credit"); m_html += "
%1%1
"; } } void showFavoriteReports() { QList reports = MyMoneyFile::instance()->reportList(); if (!reports.isEmpty()) { bool firstTime = 1; int row = 0; QList::const_iterator it_report = reports.constBegin(); while (it_report != reports.constEnd()) { if ((*it_report).isFavorite()) { if (firstTime) { m_html += QString("
%1
\n
 
\n").arg(i18n("Favorite Reports")); m_html += ""; m_html += ""; firstTime = false; } m_html += QString("") .arg(row++ & 0x01 ? "even" : "odd") .arg(link(VIEW_REPORTS, QString("?id=%1").arg((*it_report).id()))) .arg((*it_report).name()) .arg(linkend()) .arg((*it_report).comment()); } ++it_report; } if (!firstTime) m_html += "
"; m_html += i18n("Report"); m_html += ""; m_html += i18n("Comment"); m_html += "
%2%3%4%5
"; } } void showForecast() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); accList = m_forecast.accountList(); if (accList.count() > 0) { // sort the accounts by name qStableSort(accList.begin(), accList.end(), accountNameLess); auto i = 0; auto colspan = 1; //get begin day auto beginDay = QDate::currentDate().daysTo(m_forecast.beginForecastDate()); //if begin day is today skip to next cycle if (beginDay == 0) beginDay = m_forecast.accountsCycle(); // Now output header m_html += QString("
%1
\n
 
\n").arg(i18n("%1 Day Forecast", m_forecast.forecastDays())); m_html += ""; m_html += ""; auto colWidth = 55 / (m_forecast.forecastDays() / m_forecast.accountsCycle()); for (i = 0; (i*m_forecast.accountsCycle() + beginDay) <= m_forecast.forecastDays(); ++i) { m_html += QString(""; colspan++; } m_html += ""; // Now output entries i = 0; QList::ConstIterator it_account; for (it_account = accList.constBegin(); it_account != accList.constEnd(); ++it_account) { //MyMoneyAccount acc = (*it_n); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString(""; qint64 dropZero = -1; //account dropped below zero qint64 dropMinimum = -1; //account dropped below minimum balance QString minimumBalance = (*it_account).value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); MyMoneySecurity currency; MyMoneyMoney forecastBalance; //change account to deep currency if account is an investment if ((*it_account).isInvest()) { MyMoneySecurity underSecurity = file->security((*it_account).currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security((*it_account).currencyId()); } for (auto f = beginDay; f <= m_forecast.forecastDays(); f += m_forecast.accountsCycle()) { forecastBalance = m_forecast.forecastBalance(*it_account, QDate::currentDate().addDays(f)); QString amount; amount = MyMoneyUtils::formatMoney(forecastBalance, *it_account, currency); amount.replace(QChar(' '), " "); m_html += QString("").arg(showColoredAmount(amount, forecastBalance.isNegative())); } m_html += ""; //Check if the account is going to be below zero or below the minimal balance in the forecast period //Check if the account is going to be below minimal balance dropMinimum = m_forecast.daysToMinimumBalance(*it_account); //Check if the account is going to be below zero in the future dropZero = m_forecast.daysToZeroBalance(*it_account); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = i18n("The balance of %1 is below the minimum balance %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; default: msg = i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18n("The balance of %1 is below %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } break; default: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } m_html += "
"; m_html += i18n("Account"); m_html += "").arg(colWidth); m_html += i18ncp("Forecast days", "%1 day", "%1 days", i * m_forecast.accountsCycle() + beginDay); m_html += "
") + link(VIEW_LEDGER, QString("?id=%1").arg((*it_account).id())) + (*it_account).name() + linkend() + "").arg(colWidth); m_html += QString("%1
%1
%1
"; } } QString link(const QString& view, const QString& query, const QString& _title = QString()) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString linkend() const { return QStringLiteral(""); } void showAssetsLiabilities() { QList accounts; QList::ConstIterator it; QList assets; QList liabilities; MyMoneyMoney netAssets; MyMoneyMoney netLiabilities; QString fontStart, fontEnd; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int i = 0; // get list of all accounts file->accountList(accounts); for (it = accounts.constBegin(); it != accounts.constEnd();) { if (!(*it).isClosed()) { switch ((*it).accountType()) { // group all assets into one list but make sure that investment accounts always show up case Account::Type::Investment: assets << *it; break; case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Asset: case Account::Type::AssetLoan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { assets << *it; } break; // group the liabilities into the other case Account::Type::CreditCard: case Account::Type::Liability: case Account::Type::Loan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { liabilities << *it; } break; default: break; } } ++it; } //only do it if we have assets or liabilities account if (assets.count() > 0 || liabilities.count() > 0) { // sort the accounts by name qStableSort(assets.begin(), assets.end(), accountNameLess); qStableSort(liabilities.begin(), liabilities.end(), accountNameLess); QString statusHeader; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader; pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::ViewOutbox).pixmap(QSize(16,16))); statusHeader = QString("").arg(pathStatusHeader); } //print header m_html += "
" + i18n("Assets and Liabilities Summary") + "
\n
 
\n"; m_html += ""; //column titles m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; //intermediate row to separate both columns m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; QString placeHolder_Status, placeHolder_Counts; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) placeHolder_Status = ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) placeHolder_Counts = ""; if (KMyMoneySettings::showCountOfClearedTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) placeHolder_Counts += ""; //get asset and liability accounts QList::const_iterator asset_it = assets.constBegin(); QList::const_iterator liabilities_it = liabilities.constBegin(); for (; asset_it != assets.constEnd() || liabilities_it != liabilities.constEnd();) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //write an asset account if we still have any if (asset_it != assets.constEnd()) { MyMoneyMoney value; //investment accounts consolidate the balance of its subaccounts if ((*asset_it).accountType() == Account::Type::Investment) { value = investmentBalance(*asset_it); } else { value = MyMoneyFile::instance()->balance((*asset_it).id(), QDate::currentDate()); } //calculate balance for foreign currency accounts if ((*asset_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*asset_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netAssets += baseValue; } else { netAssets += value; } //show the account without minimum balance showAccountEntry(*asset_it, value, MyMoneyMoney(), false); ++asset_it; } else { //write a white space if we don't m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } //leave the intermediate column empty m_html += ""; //write a liability account if (liabilities_it != liabilities.constEnd()) { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*liabilities_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*liabilities_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*liabilities_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netLiabilities += baseValue; } else { netLiabilities += value; } //show the account without minimum balance showAccountEntry(*liabilities_it, value, MyMoneyMoney(), false); ++liabilities_it; } else { //leave the space empty if we run out of liabilities m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } m_html += ""; } //calculate net worth MyMoneyMoney netWorth = netAssets + netLiabilities; //format assets, liabilities and net worth QString amountAssets = netAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiabilities = netLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountNetWorth = netWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountAssets.replace(QChar(' '), " "); amountLiabilities.replace(QChar(' '), " "); amountNetWorth.replace(QChar(' '), " "); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //print total for assets m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Assets")).arg(placeHolder_Counts).arg(showColoredAmount(amountAssets, netAssets.isNegative())); //leave the intermediate column empty m_html += ""; //print total liabilities m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Liabilities")).arg(placeHolder_Counts).arg(showColoredAmount(amountLiabilities, netLiabilities.isNegative())); m_html += ""; //print net worth m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Net Worth")).arg(placeHolder_Counts).arg(showColoredAmount(amountNetWorth, netWorth.isNegative())); m_html += ""; m_html += "
"; m_html += statusHeader; m_html += ""; m_html += i18n("Asset Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += ""; m_html += statusHeader; m_html += ""; m_html += i18n("Liability Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += "
%2%4%2%4
%2%4
"; m_html += "
"; } } void showBudget() { m_html += "
" + i18n("Budget") + "
\n
 
\n"; m_html += ""; if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::Budget); if (!variantReport.isNull()) m_html.append(variantReport.toString()); } else { m_html += QString(""); m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); m_html += QString(""); } m_html += QString("
%1
"); } void showCashFlowSummary() { MyMoneyTransactionFilter filter; MyMoneyMoney incomeValue; MyMoneyMoney expenseValue; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); //set start and end of month dates QDate startOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1); QDate endOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().daysInMonth()); //Add total income and expenses for this month //get transactions for current month filter.setDateFilter(startOfMonth, endOfMonth); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if no transaction then skip and print total in zero if (transactions.size() > 0) { //get all transactions for this month foreach (const auto transaction, transactions) { //get the splits for each transaction foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto repSplitAcc = file->account(split.accountId()); //only add if it is an income or expense if (repSplitAcc.isIncomeExpense()) { MyMoneyMoney value; //convert to base currency if necessary if (repSplitAcc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curRate; value = value.convert(10000); } else { value = (split.shares() * MyMoneyMoney::MINUS_ONE); } //store depending on account type if (repSplitAcc.accountType() == Account::Type::Income) { incomeValue += value; } else { expenseValue += value; } } } } } } //format income and expenses QString amountIncome = incomeValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpense = expenseValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountIncome.replace(QChar(' '), " "); amountExpense.replace(QChar(' '), " "); //calculate schedules //Add all schedules for this month MyMoneyMoney scheduledIncome; MyMoneyMoney scheduledExpense; MyMoneyMoney scheduledLiquidTransfer; MyMoneyMoney scheduledOtherTransfer; //get overdues and schedules until the end of this month QList schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), endOfMonth, false); //Remove the finished schedules QList::Iterator finished_it; for (finished_it = schedule.begin(); finished_it != schedule.end();) { if ((*finished_it).isFinished()) { finished_it = schedule.erase(finished_it); continue; } ++finished_it; } //add income and expenses QList::Iterator sched_it; for (sched_it = schedule.begin(); sched_it != schedule.end();) { QDate nextDate = (*sched_it).nextDueDate(); int cnt = 0; while (nextDate.isValid() && nextDate <= endOfMonth) { ++cnt; nextDate = (*sched_it).nextPayment(nextDate); // for single occurrence nextDate will not change, so we // better get out of here. if ((*sched_it).occurrence() == Schedule::Occurrence::Once) break; } MyMoneyAccount acc = (*sched_it).account(); if (!acc.id().isEmpty()) { MyMoneyTransaction transaction = (*sched_it).transaction(); // only show the entry, if it is still active MyMoneySplit sp = transaction.splitByAccount(acc.id(), true); // take care of the autoCalc stuff if ((*sched_it).type() == Schedule::Type::LoanPayment) { nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); //make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, transaction.splits()) { acc = file->account(split.accountId()); // collect all overdues on the first day QDate schedDate = nextDate; if (QDate::currentDate() >= nextDate) schedDate = QDate::currentDate().addDays(1); balanceMap[acc.id()] += file->balance(acc.id(), QDate::currentDate()); } KMyMoneyUtils::calculateAutoLoan(*sched_it, transaction, balanceMap); } //go through the splits and assign to liquid or other transfers const QList splits = transaction.splits(); QList::const_iterator split_it; for (split_it = splits.constBegin(); split_it != splits.constEnd(); ++split_it) { if ((*split_it).accountId() != acc.id()) { auto repSplitAcc = file->account((*split_it).accountId()); //get the shares and multiply by the quantity of occurrences in the period MyMoneyMoney value = (*split_it).shares() * cnt; //convert to foreign currency if needed if (repSplitAcc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = value * curRate; value = value.convert(10000); } if ((repSplitAcc.isLiquidLiability() || repSplitAcc.isLiquidAsset()) && acc.accountGroup() != repSplitAcc.accountGroup()) { scheduledLiquidTransfer += value; } else if (repSplitAcc.isAssetLiability() && !repSplitAcc.isLiquidLiability() && !repSplitAcc.isLiquidAsset()) { scheduledOtherTransfer += value; } else if (repSplitAcc.isIncomeExpense()) { //income and expenses are stored as negative values if (repSplitAcc.accountType() == Account::Type::Income) scheduledIncome -= value; if (repSplitAcc.accountType() == Account::Type::Expense) scheduledExpense -= value; } } } } ++sched_it; } //format the currency strings QString amountScheduledIncome = scheduledIncome.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledExpense = scheduledExpense.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledLiquidTransfer = scheduledLiquidTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledOtherTransfer = scheduledOtherTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountScheduledIncome.replace(QChar(' '), " "); amountScheduledExpense.replace(QChar(' '), " "); amountScheduledLiquidTransfer.replace(QChar(' '), " "); amountScheduledOtherTransfer.replace(QChar(' '), " "); //get liquid assets and liabilities QList accounts; QList::const_iterator account_it; MyMoneyMoney liquidAssets; MyMoneyMoney liquidLiabilities; // get list of all accounts file->accountList(accounts); for (account_it = accounts.constBegin(); account_it != accounts.constEnd();) { if (!(*account_it).isClosed()) { switch ((*account_it).accountType()) { //group all assets into one list case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: { MyMoneyMoney value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance for foreign currency accounts if ((*account_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; liquidAssets += baseValue; liquidAssets = liquidAssets.convert(10000); } else { liquidAssets += value; } break; } //group the liabilities into the other case Account::Type::CreditCard: { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*account_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; liquidLiabilities += baseValue; liquidLiabilities = liquidLiabilities.convert(10000); } else { liquidLiabilities += value; } break; } default: break; } } ++account_it; } //calculate net worth MyMoneyMoney liquidWorth = liquidAssets + liquidLiabilities; //format assets, liabilities and net worth QString amountLiquidAssets = liquidAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidLiabilities = liquidLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidWorth = liquidWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountLiquidAssets.replace(QChar(' '), " "); amountLiquidLiabilities.replace(QChar(' '), " "); amountLiquidWorth.replace(QChar(' '), " "); //show the summary m_html += "
" + i18n("Cash Flow Summary") + "
\n
 
\n"; //print header m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current income m_html += QString("").arg(showColoredAmount(amountIncome, incomeValue.isNegative())); //print the scheduled income m_html += QString("").arg(showColoredAmount(amountScheduledIncome, scheduledIncome.isNegative())); //print current expenses m_html += QString("").arg(showColoredAmount(amountExpense, expenseValue.isNegative())); //print the scheduled expenses m_html += QString("").arg(showColoredAmount(amountScheduledExpense, scheduledExpense.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Income and Expenses of Current Month"); m_html += "
"; m_html += i18n("Income"); m_html += ""; m_html += i18n("Scheduled Income"); m_html += ""; m_html += i18n("Expenses"); m_html += ""; m_html += i18n("Scheduled Expenses"); m_html += "
%2%2%2%2
"; //print header of assets and liabilities m_html += "
 
\n"; m_html += ""; //assets and liabilities title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current liquid assets m_html += QString("").arg(showColoredAmount(amountLiquidAssets, liquidAssets.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledLiquidTransfer, scheduledLiquidTransfer.isNegative())); //print current liabilities m_html += QString("").arg(showColoredAmount(amountLiquidLiabilities, liquidLiabilities.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledOtherTransfer, scheduledOtherTransfer.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Liquid Assets and Liabilities"); m_html += "
"; m_html += i18n("Liquid Assets"); m_html += ""; m_html += i18n("Transfers to Liquid Liabilities"); m_html += ""; m_html += i18n("Liquid Liabilities"); m_html += ""; m_html += i18n("Other Transfers"); m_html += "
%2%2%2%2
"; //final conclusion MyMoneyMoney profitValue = incomeValue + expenseValue + scheduledIncome + scheduledExpense; MyMoneyMoney expectedAsset = liquidAssets + scheduledIncome + scheduledExpense + scheduledLiquidTransfer + scheduledOtherTransfer; MyMoneyMoney expectedLiabilities = liquidLiabilities + scheduledLiquidTransfer; QString amountExpectedAsset = expectedAsset.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpectedLiabilities = expectedLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountProfit = profitValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountProfit.replace(QChar(' '), " "); amountExpectedAsset.replace(QChar(' '), " "); amountExpectedLiabilities.replace(QChar(' '), " "); //print header of cash flow status m_html += "
 
\n"; m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); m_html += ""; //print expected assets m_html += QString("").arg(showColoredAmount(amountExpectedAsset, expectedAsset.isNegative())); //print expected liabilities m_html += QString("").arg(showColoredAmount(amountExpectedLiabilities, expectedLiabilities.isNegative())); //print expected profit m_html += QString("").arg(showColoredAmount(amountProfit, profitValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Cash Flow Status"); m_html += "
 "; m_html += i18n("Expected Liquid Assets"); m_html += ""; m_html += i18n("Expected Liquid Liabilities"); m_html += ""; m_html += i18n("Expected Profit/Loss"); m_html += "
 %2%2%2
"; m_html += "
"; } KHomeView *q_ptr; /** * daily balances of an account */ typedef QMap dailyBalances; #ifdef ENABLE_WEBENGINE QWebEngineView *m_view; #else KWebView *m_view; #endif QString m_html; bool m_showAllSchedules; bool m_needLoad; MyMoneyForecast m_forecast; MyMoneyMoney m_total; /** * Hold the last valid size of the net worth graph * for the times when the needed size can't be computed. */ QSize m_netWorthGraphLastValidSize; QMap< QString, QVector > m_transactionStats; /** * daily forecast balance of accounts */ QMap m_accountList; QPrinter *m_currentPrinter; + int m_scrollBarPos; }; #endif diff --git a/kmymoney/widgets/investtransaction.cpp b/kmymoney/widgets/investtransaction.cpp index 5497a8862..f5c8e9571 100644 --- a/kmymoney/widgets/investtransaction.cpp +++ b/kmymoney/widgets/investtransaction.cpp @@ -1,935 +1,935 @@ /* * Copyright 2006-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "investtransaction.h" #include "investtransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneypayeecombo.h" #include "transaction.h" #include "mymoneyutils.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "register.h" #include "transactionform.h" #include "kmymoneylineedit.h" #include "kmymoneyedit.h" #include "kmymoneycombo.h" #include "investtransactioneditor.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "widgetenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; InvestTransaction::InvestTransaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : Transaction(*new InvestTransactionPrivate, parent, transaction, split, uniqueId) { Q_D(InvestTransaction); #ifndef KMM_DESIGNER // dissect the transaction into its type, splits, currency, security etc. KMyMoneyUtils::dissectTransaction(d->m_transaction, d->m_split, d->m_assetAccountSplit, d->m_feeSplits, d->m_interestSplits, d->m_security, d->m_currency, d->m_transactionType); #endif QList::ConstIterator it_s; for (it_s = d->m_feeSplits.constBegin(); it_s != d->m_feeSplits.constEnd(); ++it_s) { d->m_feeAmount += (*it_s).value(); } for (it_s = d->m_interestSplits.constBegin(); it_s != d->m_interestSplits.constEnd(); ++it_s) { d->m_interestAmount += (*it_s).value(); } // check the count of the fee splits and setup the text switch (d->m_feeSplits.count()) { case 0: break; case 1: d->m_feeCategory = MyMoneyFile::instance()->accountToCategory(d->m_feeSplits[0].accountId()); break; default: d->m_feeCategory = i18nc("Split transaction (category replacement)", "Split transaction"); break; } // check the count of the interest splits and setup the text switch (d->m_interestSplits.count()) { case 0: break; case 1: d->m_interestCategory = MyMoneyFile::instance()->accountToCategory(d->m_interestSplits[0].accountId()); break; default: d->m_interestCategory = i18nc("Split transaction (category replacement)", "Split transaction"); break; } d->m_rowsForm = 7; // setup initial size setNumRowsRegister(numRowsRegister(KMyMoneySettings::showRegisterDetailed())); emit parent->itemAdded(this); } InvestTransaction::~InvestTransaction() { } const QString InvestTransaction::sortSecurity() const { Q_D(const InvestTransaction); return d->m_security.name(); } const char* InvestTransaction::className() { return "InvestTransaction"; } void InvestTransaction::setupForm(TransactionForm* form) { Transaction::setupForm(form); form->setSpan(5, 1, 2, 1); } void InvestTransaction::activity(QString& txt, eMyMoney::Split::InvestmentTransactionType type) const { switch (type) { case eMyMoney::Split::InvestmentTransactionType::AddShares: txt = i18n("Add shares"); break; case eMyMoney::Split::InvestmentTransactionType::RemoveShares: txt = i18n("Remove shares"); break; case eMyMoney::Split::InvestmentTransactionType::BuyShares: txt = i18n("Buy shares"); break; case eMyMoney::Split::InvestmentTransactionType::SellShares: txt = i18n("Sell shares"); break; case eMyMoney::Split::InvestmentTransactionType::Dividend: txt = i18n("Dividend"); break; case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: txt = i18n("Reinvest Dividend"); break; case eMyMoney::Split::InvestmentTransactionType::Yield: txt = i18n("Yield"); break; case eMyMoney::Split::InvestmentTransactionType::SplitShares: txt = i18n("Split shares"); break; case eMyMoney::Split::InvestmentTransactionType::InterestIncome: txt = i18n("Interest Income"); break; default: txt = i18nc("Unknown investment activity", "Unknown"); break; } } bool InvestTransaction::formCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* /* painter */) { Q_D(InvestTransaction); bool fieldEditable = false; switch (row) { case 0: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Activity"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; fieldEditable = true; activity(txt, d->m_transactionType); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Date"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; fieldEditable = true; - if (d->m_transaction != MyMoneyTransaction()) + if (!d->m_transaction.id().isEmpty()) txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; } break; case 1: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Security"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; fieldEditable = true; if (d->m_account.isInvest()) txt = d->m_security.name(); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveShares()) { txt = i18n("Shares"); } else if (haveSplitRatio()) { txt = i18n("Ratio"); } break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if ((fieldEditable = haveShares()) == true) { txt = d->m_split.shares().abs().formatMoney(QString(), MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); } else if (haveSplitRatio()) { txt = QString("1 / %1").arg(d->m_split.shares().abs().formatMoney(QString(), -1)); } break; } break; case 2: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; if (haveAssetAccount()) txt = i18n("Account"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if ((fieldEditable = haveAssetAccount()) == true) { txt = MyMoneyFile::instance()->accountToCategory(d->m_assetAccountSplit.accountId()); } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (havePrice()) txt = i18n("Price/share"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if ((fieldEditable = havePrice()) == true && !d->m_split.shares().isZero()) { txt = d->m_split.price().formatMoney(QString(), d->m_security.pricePrecision()); } break; } break; case 3: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; if (haveFees()) txt = i18n("Fees"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if ((fieldEditable = haveFees()) == true) { txt = d->m_feeCategory; } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveFees() && !d->m_feeCategory.isEmpty()) txt = i18n("Fee Amount"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (haveFees()) { if ((fieldEditable = !d->m_feeCategory.isEmpty()) == true) { txt = MyMoneyUtils::formatMoney(d->m_feeAmount, d->m_currency); } } break; } break; case 4: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; if (haveInterest()) txt = i18n("Interest"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if ((fieldEditable = haveInterest()) == true) { txt = d->m_interestCategory; } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveInterest() && !d->m_interestCategory.isEmpty()) txt = i18n("Interest"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (haveInterest()) { if ((fieldEditable = !d->m_interestCategory.isEmpty()) == true) { txt = MyMoneyUtils::formatMoney(-d->m_interestAmount, d->m_currency); } } break; } break; case 5: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Memo"); break; case (int)eTransactionForm::Column::Value1: align &= ~Qt::AlignVCenter; align |= Qt::AlignTop; align |= Qt::AlignLeft; fieldEditable = true; - if (d->m_transaction != MyMoneyTransaction()) + if (!d->m_transaction.id().isEmpty()) txt = d->m_split.memo().section('\n', 0, 2); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveAmount()) txt = i18nc("Total balance", "Total"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if ((fieldEditable = haveAmount()) == true) { txt = d->m_assetAccountSplit.value().abs() .formatMoney(d->m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction())); } } break; case 6: switch (col) { case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Status"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; fieldEditable = true; txt = reconcileState(); break; } } return fieldEditable; } void InvestTransaction::registerCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* /* painter */) { Q_D(InvestTransaction); switch (row) { case 0: switch (col) { case (int)eTransaction::Column::Date: align |= Qt::AlignLeft; txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; activity(txt, d->m_transactionType); break; case (int)eTransaction::Column::Security: align |= Qt::AlignLeft; if (d->m_account.isInvest()) txt = d->m_security.name(); break; case (int)eTransaction::Column::ReconcileFlag: align |= Qt::AlignHCenter; txt = reconcileState(false); break; case (int)eTransaction::Column::Quantity: align |= Qt::AlignRight; if (haveShares()) txt = d->m_split.shares().abs().formatMoney(QString(), MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else if (haveSplitRatio()) { txt = QString("1 / %1").arg(d->m_split.shares().abs().formatMoney(QString(), -1)); } break; case (int)eTransaction::Column::Price: align |= Qt::AlignRight; if (havePrice() && !d->m_split.shares().isZero()) { txt = d->m_split.price().formatMoney(d->m_currency.tradingSymbol(), d->m_security.pricePrecision()); } break; case (int)eTransaction::Column::Value: align |= Qt::AlignRight; if (haveAmount()) { txt = MyMoneyUtils::formatMoney(d->m_assetAccountSplit.value().abs(), d->m_currency); } else if (haveInterest()) { txt = MyMoneyUtils::formatMoney(-d->m_interestAmount, d->m_currency); } break; case (int)eTransaction::Column::Balance: align |= Qt::AlignRight; if (d->m_showBalance) txt = d->m_balance.formatMoney(QString(), MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else txt = "----"; break; default: break; } break; case 1: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty()) { txt = MyMoneyFile::instance()->accountToCategory(d->m_assetAccountSplit.accountId()); } else if (haveInterest() && d->m_interestSplits.count()) { txt = d->m_interestCategory; } else if (haveFees() && d->m_feeSplits.count()) { txt = d->m_feeCategory; } else singleLineMemo(txt, d->m_split); break; case (int)eTransaction::Column::Quantity: align |= Qt::AlignRight; if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty()) { // txt = m_interestAmount.abs().formatMoney(m_currency); } else if (haveInterest() && d->m_interestSplits.count()) { txt = MyMoneyUtils::formatMoney(-d->m_interestAmount, d->m_currency); } else if (haveFees() && d->m_feeSplits.count()) { txt = MyMoneyUtils::formatMoney(d->m_feeAmount, d->m_currency); } break; default: break; } break; case 2: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty() && haveInterest() && d->m_interestSplits.count()) { txt = d->m_interestCategory; } else if (haveFees() && d->m_feeSplits.count()) { txt = d->m_feeCategory; } else singleLineMemo(txt, d->m_split); break; case (int)eTransaction::Column::Quantity: align |= Qt::AlignRight; if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty() && haveInterest() && d->m_interestSplits.count()) { txt = MyMoneyUtils::formatMoney(-d->m_interestAmount, d->m_currency); } else if (haveFees() && d->m_feeSplits.count()) { txt = MyMoneyUtils::formatMoney(d->m_feeAmount, d->m_currency); } break; default: break; } break; case 3: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty() && haveInterest() && d->m_interestSplits.count() && haveFees() && d->m_feeSplits.count()) { txt = d->m_feeCategory; } else singleLineMemo(txt, d->m_split); break; case (int)eTransaction::Column::Quantity: align |= Qt::AlignRight; if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty() && haveInterest() && d->m_interestSplits.count() && haveFees() && d->m_feeSplits.count()) { txt = MyMoneyUtils::formatMoney(d->m_feeAmount, d->m_currency); } break; default: break; } break; case 4: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; singleLineMemo(txt, d->m_split); break; default: break; } break; } } int InvestTransaction::registerColWidth(int col, const QFontMetrics& cellFontMetrics) { Q_D(InvestTransaction); QString txt; MyMoneyMoney amount; int nw = 0; // for now just check all rows in that column for (int row = 0; row < d->m_rowsRegister; ++row) { int w; Transaction::registerCellText(txt, row, col); w = cellFontMetrics.width(txt + " "); nw = qMax(nw, w); } // TODO the optimized way would be to base the size on the contents of a single row // as we do it in StdTransaction::registerColWidth() #if 0 switch (col) { default: break; case PriceColumn: if (havePrice()) { txt = (m_split.value() / m_split.shares()).formatMoney(QString(), KMyMoneySettings::pricePrecision()); nw = cellFontMetrics.width(txt + " "); } break; } #endif return nw; } void InvestTransaction::loadTab(KMyMoneyTransactionForm::TransactionForm* /* form */) { } int InvestTransaction::numColsForm() const { return 4; } void InvestTransaction::arrangeWidgetsInForm(QMap& editWidgets) { Q_D(InvestTransaction); if (!d->m_form || !d->m_parent) return; setupFormPalette(editWidgets); // arrange the edit widgets arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Value1, editWidgets["activity"]); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Value2, editWidgets["postdate"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value1, editWidgets["security"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value2, editWidgets["shares"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value1, editWidgets["asset-account"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value2, editWidgets["price"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value1, editWidgets["fee-account"]->parentWidget()); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value2, editWidgets["fee-amount"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Value1, editWidgets["interest-account"]->parentWidget()); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Value2, editWidgets["interest-amount"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Value1, editWidgets["memo"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Value2, editWidgets["total"]); arrangeWidget(d->m_form, 6, (int)eTransactionForm::Column::Value2, editWidgets["status"]); // arrange dynamic labels arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Label1, editWidgets["activity-label"]); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Label2, editWidgets["postdate-label"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label1, editWidgets["security-label"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label2, editWidgets["shares-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label1, editWidgets["asset-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label2, editWidgets["price-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label1, editWidgets["fee-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label2, editWidgets["fee-amount-label"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Label1, editWidgets["interest-label"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Label2, editWidgets["interest-amount-label"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Label1, editWidgets["memo-label"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Label2, editWidgets["total-label"]); arrangeWidget(d->m_form, 6, (int)eTransactionForm::Column::Label2, editWidgets["status-label"]); // get rid of the hints. we don't need them for the form QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end(); ++it) { KMyMoneyCombo* combo = dynamic_cast(*it); KMyMoneyLineEdit* lineedit = dynamic_cast(*it); KMyMoneyEdit* edit = dynamic_cast(*it); KMyMoneyPayeeCombo* payee = dynamic_cast(*it); if (combo) combo->setPlaceholderText(QString()); if (edit) edit->setPlaceholderText(QString()); if (lineedit) lineedit->setPlaceholderText(QString()); if (payee) payee->setPlaceholderText(QString()); } } void InvestTransaction::tabOrderInForm(QWidgetList& tabOrderWidgets) const { Q_D(const InvestTransaction); // activity tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(0, (int)eTransactionForm::Column::Value1))); // date tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(0, (int)eTransactionForm::Column::Value2))); // security tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value1))); // shares tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value2))); // account tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value1))); // price tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value2))); // make sure to have the fee category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... QWidget* w = d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value1); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); // fee amount tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value2))); // the same applies for the interest categories w = d->m_form->cellWidget(4, (int)eTransactionForm::Column::Value1); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); // interest amount tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(4, (int)eTransactionForm::Column::Value2))); // memo tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(5, (int)eTransactionForm::Column::Value1))); // state tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(6, (int)eTransactionForm::Column::Value2))); } void InvestTransaction::arrangeWidgetsInRegister(QMap& editWidgets) { Q_D(InvestTransaction); if (!d->m_parent) return; setupRegisterPalette(editWidgets); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Date, editWidgets["postdate"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Security, editWidgets["security"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Detail, editWidgets["activity"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Detail, editWidgets["asset-account"]); arrangeWidget(d->m_parent, d->m_startRow + 2, (int)eTransaction::Column::Detail, editWidgets["interest-account"]->parentWidget()); arrangeWidget(d->m_parent, d->m_startRow + 3, (int)eTransaction::Column::Detail, editWidgets["fee-account"]->parentWidget()); arrangeWidget(d->m_parent, d->m_startRow + 4, (int)eTransaction::Column::Detail, editWidgets["memo"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Quantity, editWidgets["shares"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Price, editWidgets["price"]); arrangeWidget(d->m_parent, d->m_startRow + 2, (int)eTransaction::Column::Quantity, editWidgets["interest-amount"]); arrangeWidget(d->m_parent, d->m_startRow + 3, (int)eTransaction::Column::Quantity, editWidgets["fee-amount"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Value, editWidgets["total"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Date, editWidgets["status"]); // increase the height of the row containing the memo widget d->m_parent->setRowHeight(d->m_startRow + 4, d->m_parent->rowHeightHint() * 3); } void InvestTransaction::tabOrderInRegister(QWidgetList& tabOrderWidgets) const { Q_D(const InvestTransaction); QWidget* w; // date tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Date))); // security tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Security))); // activity tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Detail))); // shares tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Quantity))); // price tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Price))); // asset account tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Detail))); // make sure to have the category fields and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... w = d->m_parent->cellWidget(d->m_startRow + 2, (int)eTransaction::Column::Detail); // interest account tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); // interest amount tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 2, (int)eTransaction::Column::Quantity))); w = d->m_parent->cellWidget(d->m_startRow + 3, (int)eTransaction::Column::Detail); // fee account tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); // fee amount tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 3, (int)eTransaction::Column::Quantity))); // memo tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 4, (int)eTransaction::Column::Detail))); // status tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Date))); } eRegister::Action InvestTransaction::actionType() const { return eRegister::Action::None; } int InvestTransaction::numRowsRegister(bool expanded) const { Q_D(const InvestTransaction); int numRows = 1; if (expanded) { if (!d->m_inEdit) { if (haveAssetAccount() && !d->m_assetAccountSplit.accountId().isEmpty()) ++numRows; if (haveInterest() && d->m_interestSplits.count()) ++numRows; if (haveFees() && d->m_feeSplits.count()) ++numRows; if (!d->m_split.memo().isEmpty()) ++numRows; } else numRows = 5; } return numRows; } bool InvestTransaction::haveShares() const { Q_D(const InvestTransaction); auto rc = true; switch (d->m_transactionType) { case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::Yield: case eMyMoney::Split::InvestmentTransactionType::SplitShares: case eMyMoney::Split::InvestmentTransactionType::InterestIncome: rc = false; break; default: break; } return rc; } bool InvestTransaction::haveFees() const { Q_D(const InvestTransaction); auto rc = true; switch (d->m_transactionType) { case eMyMoney::Split::InvestmentTransactionType::AddShares: case eMyMoney::Split::InvestmentTransactionType::RemoveShares: case eMyMoney::Split::InvestmentTransactionType::SplitShares: rc = false; break; default: break; } return rc; } bool InvestTransaction::haveInterest() const { Q_D(const InvestTransaction); auto rc = false; switch (d->m_transactionType) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::SellShares: case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: case eMyMoney::Split::InvestmentTransactionType::Yield: case eMyMoney::Split::InvestmentTransactionType::InterestIncome: rc = true; break; default: break; } return rc; } bool InvestTransaction::havePrice() const { Q_D(const InvestTransaction); auto rc = false; switch (d->m_transactionType) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::SellShares: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: rc = true; break; default: break; } return rc; } bool InvestTransaction::haveAmount() const { Q_D(const InvestTransaction); auto rc = false; switch (d->m_transactionType) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::SellShares: case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::Yield: case eMyMoney::Split::InvestmentTransactionType::InterestIncome: rc = true; break; default: break; } return rc; } bool InvestTransaction::haveAssetAccount() const { Q_D(const InvestTransaction); auto rc = true; switch (d->m_transactionType) { case eMyMoney::Split::InvestmentTransactionType::AddShares: case eMyMoney::Split::InvestmentTransactionType::RemoveShares: case eMyMoney::Split::InvestmentTransactionType::SplitShares: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: rc = false; break; default: break; } return rc; } bool InvestTransaction::haveSplitRatio() const { Q_D(const InvestTransaction); return d->m_transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares; } void InvestTransaction::splits(MyMoneySplit& assetAccountSplit, QList& interestSplits, QList& feeSplits) const { Q_D(const InvestTransaction); assetAccountSplit = d->m_assetAccountSplit; interestSplits = d->m_interestSplits; feeSplits = d->m_feeSplits; } int InvestTransaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } TransactionEditor* InvestTransaction::createEditor(TransactionEditorContainer* regForm, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) { #ifndef KMM_DESIGNER Q_D(InvestTransaction); d->m_inRegisterEdit = regForm == d->m_parent; return new InvestTransactionEditor(regForm, this, list, lastPostDate); #else return NULL; #endif } diff --git a/kmymoney/widgets/stdtransaction.cpp b/kmymoney/widgets/stdtransaction.cpp index e2dd088bc..729f89b3c 100644 --- a/kmymoney/widgets/stdtransaction.cpp +++ b/kmymoney/widgets/stdtransaction.cpp @@ -1,706 +1,706 @@ /* * Copyright 2006-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "stdtransaction.h" #include "stdtransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneypayeecombo.h" #include "kmymoneycombo.h" #include "kmymoneytagcombo.h" #include "tabbar.h" #include "ktagcontainer.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "register.h" #include "transactionform.h" #include "kmymoneylineedit.h" #include "kmymoneyutils.h" #ifndef KMM_DESIGNER #include "stdtransactioneditor.h" #endif #include "kmymoneysettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; StdTransaction::StdTransaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : Transaction(*new StdTransactionPrivate, parent, transaction, split, uniqueId) { Q_D(StdTransaction); d->m_showAccountRow = false; try { d->m_categoryHeader = i18n("Category"); switch (transaction.splitCount()) { default: d->m_category = i18nc("Split transaction (category replacement)", "Split transaction"); break; case 0: // the empty transaction case 1: break; case 2: setupFormHeader(d->m_transaction.splitByAccount(d->m_split.accountId(), false).accountId()); break; } } catch (const MyMoneyException &e) { qDebug() << "Problem determining the category for transaction '" << d->m_transaction.id() << "'. Reason: " << e.what() << "\n"; } d->m_rowsForm = 6; if (KMyMoneyUtils::transactionType(d->m_transaction) == KMyMoneyUtils::InvestmentTransaction) { MyMoneySplit stockSplit = KMyMoneyUtils::stockSplit(d->m_transaction); d->m_payee = MyMoneyFile::instance()->account(stockSplit.accountId()).name(); QString addon; if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { if (stockSplit.value().isNegative()) { addon = i18n("Sell"); } else { addon = i18n("Buy"); } } else if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { addon = i18n("Dividend"); } else if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { addon = i18n("Yield"); } else if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { addon = i18n("Interest Income"); } if (!addon.isEmpty()) { d->m_payee += QString(" (%1)").arg(addon); } d->m_payeeHeader = i18n("Activity"); d->m_category = i18n("Investment transaction"); } // setup initial size setNumRowsRegister(numRowsRegister(KMyMoneySettings::showRegisterDetailed())); emit parent->itemAdded(this); } StdTransaction::~StdTransaction() { } const char* StdTransaction::className() { return "StdTransaction"; } void StdTransaction::setupFormHeader(const QString& id) { Q_D(StdTransaction); d->m_category = MyMoneyFile::instance()->accountToCategory(id); switch (MyMoneyFile::instance()->account(id).accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: d->m_categoryHeader = d->m_split.shares().isNegative() ? i18n("Transfer to") : i18n("Transfer from"); break; default: d->m_categoryHeader = i18n("Category"); break; } } eRegister::Action StdTransaction::actionType() const { Q_D(const StdTransaction); eRegister::Action action = eRegister::Action::None; // if at least one split is referencing an income or // expense account, we will not call it a transfer auto found = false; foreach (const auto split, d->m_transaction.splits()) { if (split.accountId() == d->m_split.accountId()) continue; auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { // otherwise, we have to determine between deposit and withdrawal action = d->m_split.shares().isNegative() ? eRegister::Action::Withdrawal : eRegister::Action::Deposit; found = true; break; } } // otherwise, it's a transfer if (!found) action = eRegister::Action::Transfer; return action; } void StdTransaction::loadTab(TransactionForm* form) { Q_D(StdTransaction); KMyMoneyTransactionForm::TabBar* bar = form->getTabBar(); bar->setSignalEmission(eTabBar::SignalEmission::Never); for (auto i = 0; i < bar->count(); ++i) { bar->setTabEnabled(i, true); } if (d->m_transaction.splitCount() > 0) { bar->setCurrentIndex((int)actionType()); } bar->setSignalEmission(eTabBar::SignalEmission::Always); } int StdTransaction::numColsForm() const { return 4; } void StdTransaction::setupForm(TransactionForm* form) { Transaction::setupForm(form); form->setSpan(4, (int)eTransactionForm::Column::Value1, 3, 1); } bool StdTransaction::showRowInForm(int row) const { Q_D(const StdTransaction); return row == 0 ? d->m_showAccountRow : true; } void StdTransaction::setShowRowInForm(int row, bool show) { Q_D(StdTransaction); if (row == 0) d->m_showAccountRow = show; } bool StdTransaction::formCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* /* painter */) { Q_D(const StdTransaction); // if(m_transaction != MyMoneyTransaction()) { switch (row) { case 0: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Account"); break; } break; case 1: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_payeeHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_payee; break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveNumberField()) txt = i18n("Number"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (haveNumberField()) txt = d->m_split.number(); break; } break; case 2: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_categoryHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_category; - if (d->m_transaction != MyMoneyTransaction()) { + if (!d->m_transaction.id().isEmpty()) { if (txt.isEmpty() && !d->m_split.value().isZero()) txt = i18n("*** UNASSIGNED ***"); } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Date"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; - if (d->m_transaction != MyMoneyTransaction()) + if (!d->m_transaction.id().isEmpty()) txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; } break; case 3: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Tags"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if (!d->m_tagList.isEmpty()) { for (auto i = 0; i < d->m_tagList.size() - 1; ++i) txt += d->m_tagList[i] + ", "; txt += d->m_tagList.last(); } //if (m_transaction != MyMoneyTransaction()) // txt = m_split.tagId(); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Amount"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; - if (d->m_transaction != MyMoneyTransaction()) { + if (!d->m_transaction.id().isEmpty()) { txt = (d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).abs()).formatMoney(d->m_account.fraction()); } break; } break; case 4: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Memo"); break; case (int)eTransactionForm::Column::Value1: align &= ~Qt::AlignVCenter; align |= Qt::AlignTop; align |= Qt::AlignLeft; - if (d->m_transaction != MyMoneyTransaction()) + if (!d->m_transaction.id().isEmpty()) txt = d->m_split.memo().section('\n', 0, 2); break; } break; case 5: switch (col) { case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Status"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; txt = reconcileState(); break; } } // } if (col == (int)eTransactionForm::Column::Value2 && row == 1) { return haveNumberField(); } return (col == (int)eTransactionForm::Column::Value1 && row < 5) || (col == (int)eTransactionForm::Column::Value2 && row > 0 && row != 4); } void StdTransaction::registerCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* painter) { Q_D(const StdTransaction); switch (row) { case 0: switch (col) { case (int)eTransaction::Column::Number: align |= Qt::AlignLeft; if (haveNumberField()) txt = d->m_split.number(); break; case (int)eTransaction::Column::Date: align |= Qt::AlignLeft; txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_payee; break; case eRegister::DetailColumn::AccountFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; } align |= Qt::AlignLeft; if (txt.isEmpty() && d->m_rowsRegister < 3) { singleLineMemo(txt, d->m_split); } if (txt.isEmpty() && d->m_rowsRegister < 2) { if (d->m_account.accountType() != eMyMoney::Account::Type::Income && d->m_account.accountType() != eMyMoney::Account::Type::Expense) { txt = d->m_category; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } } } break; case (int)eTransaction::Column::ReconcileFlag: align |= Qt::AlignHCenter; txt = reconcileState(false); break; case (int)eTransaction::Column::Payment: align |= Qt::AlignRight; if (d->m_split.value().isNegative()) { txt = (-d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId)).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Deposit: align |= Qt::AlignRight; if (!d->m_split.value().isNegative()) { txt = d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Balance: align |= Qt::AlignRight; if (d->m_showBalance) txt = d->m_balance.formatMoney(d->m_account.fraction()); else txt = "----"; break; case (int)eTransaction::Column::Account: // txt = m_objects->account(m_transaction.splits()[0].accountId()).name(); txt = MyMoneyFile::instance()->account(d->m_split.accountId()).name(); break; default: break; } break; case 1: switch (col) { case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_category; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; case eRegister::DetailColumn::AccountFirst: txt = d->m_payee; if (txt.isEmpty()) { singleLineMemo(txt, d->m_split); } break; } align |= Qt::AlignLeft; break; default: break; } break; case 2: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; singleLineMemo(txt, d->m_split); break; default: break; } break; } } int StdTransaction::registerColWidth(int col, const QFontMetrics& cellFontMetrics) { QString txt; int firstRow = 0, lastRow = numRowsRegister(); int nw = 0; for (int i = firstRow; i <= lastRow; ++i) { Qt::Alignment align; registerCellText(txt, align, i, col, 0); int w = cellFontMetrics.width(txt + " "); if (w > nw) nw = w; } return nw; } void StdTransaction::arrangeWidgetsInForm(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_form || !d->m_parent) return; setupFormPalette(editWidgets); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Label1, editWidgets["account-label"]); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Value1, editWidgets["account"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label1, editWidgets["cashflow"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value1, editWidgets["payee"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label1, editWidgets["category-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value1, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label1, editWidgets["tag-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value1, editWidgets["tag"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Label1, editWidgets["memo-label"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Value1, editWidgets["memo"]); if (haveNumberField()) { arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label2, editWidgets["number-label"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value2, editWidgets["number"]); } arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label2, editWidgets["date-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value2, editWidgets["postdate"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label2, editWidgets["amount-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value2, editWidgets["amount"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Label2, editWidgets["status-label"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Value2, editWidgets["status"]); // get rid of the hints. we don't need them for the form QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end(); ++it) { KMyMoneyCombo* combo = dynamic_cast(*it); KMyMoneyLineEdit* edit = dynamic_cast(*it); KMyMoneyPayeeCombo* payee = dynamic_cast(*it); KTagContainer* tag = dynamic_cast(*it); if (combo) combo->setPlaceholderText(QString()); if (edit) edit->setPlaceholderText(QString()); if (payee) payee->setPlaceholderText(QString()); if (tag) tag->tagCombo()->setPlaceholderText(QString()); } auto form = dynamic_cast(d->m_form); auto w = dynamic_cast(editWidgets["tabbar"]); if (w && form) { // insert the tabbar in the boxlayout so it will take the place of the original tabbar which was hidden if (auto boxLayout = dynamic_cast(form->getTabBar()->parentWidget()->layout())) boxLayout->insertWidget(0, w); } } void StdTransaction::tabOrderInForm(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneySettings::stdTransactionFormTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "account") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(0, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "cashflow") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Label1))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... w = d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value1); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(4, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value2)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "amount") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(5, (int)eTransactionForm::Column::Value2))); } ++it_s; } } void StdTransaction::arrangeWidgetsInRegister(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_parent) return; setupRegisterPalette(editWidgets); if (haveNumberField()) arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Number, editWidgets["number"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Date, editWidgets["postdate"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Date, editWidgets["status"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Detail, editWidgets["payee"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Detail, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_parent, d->m_startRow + 2, (int)eTransaction::Column::Detail, editWidgets["tag"]); arrangeWidget(d->m_parent, d->m_startRow + 3, (int)eTransaction::Column::Detail, editWidgets["memo"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Payment, editWidgets["payment"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Deposit, editWidgets["deposit"]); // increase the height of the row containing the memo widget d->m_parent->setRowHeight(d->m_startRow + 3, d->m_parent->rowHeightHint() * 3); } void StdTransaction::tabOrderInRegister(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneySettings::stdTransactionRegisterTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Number)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Date))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Detail))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... w = d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Detail); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 2, (int)eTransaction::Column::Detail))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 3, (int)eTransaction::Column::Detail))); } else if (*it_s == "payment") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Payment))); } else if (*it_s == "deposit") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Deposit))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Date))); } ++it_s; } } int StdTransaction::numRowsRegister(bool expanded) const { Q_D(const StdTransaction); int numRows = 1; if (expanded) { numRows = 4; if (!d->m_inEdit) { //When not in edit Tags haven't a separate row; numRows--; if (d->m_payee.isEmpty()) { numRows--; } if (d->m_split.memo().isEmpty()) { numRows--; } // For income and expense accounts that only have // two splits we only show one line, because the // account name is already contained in the account column. if (d->m_account.accountType() == eMyMoney::Account::Type::Income || d->m_account.accountType() == eMyMoney::Account::Type::Expense) { if (numRows > 2 && d->m_transaction.splitCount() == 2) numRows = 1; } } } return numRows; } int StdTransaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } TransactionEditor* StdTransaction::createEditor(TransactionEditorContainer* regForm, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) { #ifndef KMM_DESIGNER Q_D(StdTransaction); d->m_inRegisterEdit = regForm == d->m_parent; return new StdTransactionEditor(regForm, this, list, lastPostDate); #else return NULL; #endif } diff --git a/kmymoney/widgets/transaction.cpp b/kmymoney/widgets/transaction.cpp index d5878e766..bbcd10b92 100644 --- a/kmymoney/widgets/transaction.cpp +++ b/kmymoney/widgets/transaction.cpp @@ -1,946 +1,946 @@ /* * Copyright 2006-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "transaction.h" #include "transaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "register.h" #include "kmymoneycategory.h" #include "kmymoneydateinput.h" #include "transactionform.h" #include "kmymoneyedit.h" #include "kmymoneyutils.h" #include "registerfilter.h" #include "tabbar.h" #include "kmymoneysettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; static unsigned char attentionSign[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x08, 0x06, 0x00, 0x00, 0x00, 0x8D, 0x89, 0x1D, 0x0D, 0x00, 0x00, 0x00, 0x04, 0x73, 0x42, 0x49, 0x54, 0x08, 0x08, 0x08, 0x08, 0x7C, 0x08, 0x64, 0x88, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x6E, 0x6B, 0x73, 0x63, 0x61, 0x70, 0x65, 0x2E, 0x6F, 0x72, 0x67, 0x9B, 0xEE, 0x3C, 0x1A, 0x00, 0x00, 0x02, 0x05, 0x49, 0x44, 0x41, 0x54, 0x38, 0x8D, 0xAD, 0x95, 0xBF, 0x4B, 0x5B, 0x51, 0x14, 0xC7, 0x3F, 0x2F, 0xBC, 0x97, 0x97, 0x97, 0x97, 0x77, 0xF3, 0xF2, 0x1C, 0xA4, 0x54, 0x6B, 0x70, 0x10, 0x44, 0x70, 0x2A, 0x91, 0x2E, 0x52, 0x02, 0x55, 0x8A, 0xB5, 0xA3, 0xAB, 0x38, 0x08, 0x66, 0xCC, 0xEE, 0xE0, 0xE2, 0x20, 0xB8, 0x38, 0xB8, 0xB8, 0xF8, 0x1F, 0x38, 0x29, 0xA5, 0x29, 0x74, 0x90, 0x0E, 0x0D, 0x0E, 0x22, 0x1D, 0x44, 0xA8, 0xD0, 0xD4, 0xB4, 0x58, 0x4B, 0x09, 0xF9, 0xF1, 0x4A, 0x3B, 0xD4, 0xD3, 0xE1, 0x55, 0xD3, 0x34, 0xAF, 0x49, 0x6C, 0x3D, 0xF0, 0x85, 0x7B, 0xCF, 0xFD, 0x9E, 0xEF, 0x3D, 0xE7, 0xFE, 0xD4, 0x44, 0x84, 0xDB, 0xB4, 0x48, 0x2F, 0xA4, 0x94, 0xAB, 0xE5, 0x52, 0xAE, 0x96, 0xEB, 0x49, 0x51, 0x44, 0x3A, 0x02, 0x18, 0x88, 0xC7, 0xF1, 0xE3, 0x71, 0x7C, 0x60, 0xA0, 0x1B, 0xBF, 0x6B, 0x86, 0x49, 0xC5, 0x46, 0x3E, 0x47, 0x34, 0x9F, 0x23, 0x9A, 0x54, 0x6C, 0xFC, 0x57, 0x86, 0x40, 0xC6, 0x4B, 0xE1, 0x37, 0xCA, 0x48, 0xA3, 0x8C, 0x78, 0x29, 0x7C, 0x20, 0xD3, 0x31, 0xA6, 0xD3, 0xA0, 0x52, 0x1C, 0x6D, 0x6F, 0x72, 0xD9, 0x28, 0x23, 0xFE, 0x07, 0x64, 0x7B, 0x93, 0x4B, 0xA5, 0x38, 0xFA, 0x27, 0x41, 0x60, 0x6E, 0x74, 0x84, 0x7A, 0xE5, 0x1D, 0x92, 0x54, 0x88, 0xE7, 0x22, 0xD5, 0x12, 0x32, 0x3A, 0x42, 0x1D, 0x98, 0xBB, 0x91, 0x20, 0x60, 0xDA, 0x36, 0x17, 0xFB, 0x7B, 0xC8, 0xC1, 0x4B, 0x04, 0x02, 0xBC, 0x7E, 0x81, 0xEC, 0xEF, 0x21, 0xB6, 0xCD, 0x05, 0x60, 0xF6, 0x2C, 0x68, 0x9A, 0x2C, 0xCF, 0x4C, 0xE1, 0x4B, 0x05, 0x39, 0x3F, 0x69, 0x0A, 0xBE, 0x7F, 0x83, 0x48, 0x05, 0x99, 0x99, 0xC2, 0x37, 0x4D, 0x96, 0x7B, 0x12, 0x04, 0xFA, 0x2D, 0x8B, 0xC6, 0xE9, 0x61, 0x10, 0x2C, 0x15, 0xC4, 0x8A, 0x21, 0x86, 0x8E, 0xFC, 0xF8, 0x12, 0xF4, 0x4F, 0x0F, 0x11, 0xCB, 0xA2, 0x01, 0xF4, 0x77, 0x3D, 0x36, 0x4E, 0x82, 0xF5, 0xA5, 0x05, 0x8C, 0xE1, 0x74, 0xD3, 0x37, 0x34, 0x18, 0x20, 0xF2, 0x8B, 0x3D, 0x9C, 0x86, 0xA5, 0x05, 0x0C, 0x27, 0xC1, 0x7A, 0xC7, 0x63, 0x03, 0x8C, 0x2B, 0x07, 0xBF, 0x5A, 0x6A, 0x66, 0x27, 0x15, 0x64, 0x3A, 0x8B, 0x3C, 0x7A, 0xD8, 0xEA, 0xAB, 0x96, 0x10, 0xE5, 0xE0, 0x03, 0xE3, 0x7F, 0xCD, 0x50, 0x39, 0x6C, 0xAD, 0xAD, 0x10, 0x53, 0xAA, 0x75, 0xD2, 0xF4, 0xBD, 0x00, 0x2D, 0x5C, 0x05, 0x6B, 0x2B, 0xC4, 0x94, 0xC3, 0xD6, 0xEF, 0xFE, 0x6B, 0x41, 0x4D, 0xD3, 0x66, 0xFB, 0x3C, 0xC6, 0x16, 0xE7, 0xDB, 0x97, 0x61, 0xE2, 0x3E, 0x3C, 0xC8, 0xB4, 0x15, 0xC7, 0xE2, 0x3C, 0x91, 0x3E, 0x8F, 0x31, 0x4D, 0xD3, 0x66, 0x5B, 0x4A, 0x06, 0x8C, 0x84, 0xCD, 0x59, 0x61, 0xA7, 0xB5, 0xAC, 0x2B, 0x9C, 0x1C, 0x04, 0x08, 0x1B, 0x2B, 0xEC, 0x20, 0x09, 0x9B, 0x33, 0xC0, 0xB8, 0xDE, 0x65, 0x43, 0x27, 0x9F, 0x9D, 0xA4, 0x1E, 0x16, 0xF0, 0xF9, 0x6D, 0xB0, 0xC3, 0x86, 0x1E, 0xB4, 0xC3, 0x38, 0xD9, 0x49, 0xEA, 0x86, 0x4E, 0xFE, 0xEA, 0x29, 0xF4, 0x2C, 0x8B, 0xDA, 0x71, 0x31, 0x9C, 0xFC, 0xF5, 0x23, 0x32, 0x34, 0x88, 0xDC, 0xBD, 0x13, 0x5C, 0xBF, 0x30, 0xCE, 0x71, 0x11, 0xB1, 0x2C, 0x6A, 0x80, 0xA7, 0xDB, 0x36, 0xAB, 0x4F, 0xA6, 0x89, 0xBA, 0x49, 0x38, 0xFF, 0xD4, 0xBE, 0x4E, 0x00, 0xAF, 0x9E, 0x81, 0x08, 0xD4, 0xEA, 0x01, 0xFE, 0x34, 0x37, 0x09, 0x4F, 0x1F, 0x13, 0xDD, 0x7D, 0xCE, 0xAA, 0x96, 0x72, 0x29, 0x7C, 0xFB, 0xCE, 0x44, 0xB8, 0xD4, 0xCD, 0x2C, 0x66, 0x52, 0xD4, 0x6E, 0xFB, 0x0B, 0xF8, 0x09, 0x63, 0x63, 0x31, 0xE4, 0x85, 0x76, 0x2E, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; Transaction::Transaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : RegisterItem(*new TransactionPrivate, parent) { Q_D(Transaction); d->m_transaction = transaction; d->m_split = split; d->m_form = nullptr; d->m_uniqueId = d->m_transaction.id(); d->init(uniqueId); } Transaction::Transaction(TransactionPrivate &dd, Register* parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : RegisterItem(dd, parent) { Q_D(Transaction); d->m_form = nullptr; d->m_transaction = transaction; d->m_split = split; d->m_uniqueId = d->m_transaction.id(); d->init(uniqueId); } Transaction::~Transaction() { } const char* Transaction::className() { return "Transaction"; } bool Transaction::isSelectable() const { return true; } bool Transaction::isSelected() const { Q_D(const Transaction); return d->m_selected; } void Transaction::setSelected(bool selected) { Q_D(Transaction); if (!selected || (selected && isVisible())) d->m_selected = selected; } bool Transaction::canHaveFocus() const { return true; } bool Transaction::hasFocus() const { Q_D(const Transaction); return d->m_focus; } bool Transaction::hasEditorOpen() const { Q_D(const Transaction); return d->m_inEdit; } bool Transaction::isScheduled() const { return false; } void Transaction::setFocus(bool focus, bool updateLens) { Q_D(Transaction); if (focus != d->m_focus) { d->m_focus = focus; } if (updateLens) { if (KMyMoneySettings::ledgerLens() || !KMyMoneySettings::transactionForm() || KMyMoneySettings::showRegisterDetailed() || d->m_parent->ledgerLens()) { if (focus) setNumRowsRegister(numRowsRegister(true)); else setNumRowsRegister(numRowsRegister(KMyMoneySettings::showRegisterDetailed())); } } } bool Transaction::isErroneous() const { Q_D(const Transaction); return d->m_erroneous; } QDate Transaction::sortPostDate() const { Q_D(const Transaction); return d->m_transaction.postDate(); } int Transaction::sortSamePostDate() const { return 2; } QDate Transaction::sortEntryDate() const { Q_D(const Transaction); return d->m_transaction.entryDate(); } const QString& Transaction::sortPayee() const { Q_D(const Transaction); return d->m_payee; } const QList& Transaction::sortTagList() const { Q_D(const Transaction); return d->m_tagList; } MyMoneyMoney Transaction::sortValue() const { Q_D(const Transaction); return d->m_split.shares(); } QString Transaction::sortNumber() const { Q_D(const Transaction); return d->m_split.number(); } const QString& Transaction::sortEntryOrder() const { Q_D(const Transaction); return d->m_uniqueId; } eRegister::CashFlowDirection Transaction::sortType() const { Q_D(const Transaction); return d->m_split.shares().isNegative() ? eRegister::CashFlowDirection::Payment : eRegister::CashFlowDirection::Deposit; } const QString& Transaction::sortCategory() const { Q_D(const Transaction); return d->m_category; } eMyMoney::Split::State Transaction::sortReconcileState() const { Q_D(const Transaction); return d->m_split.reconcileFlag(); } QString Transaction::id() const { Q_D(const Transaction); return d->m_uniqueId; } const MyMoneyTransaction& Transaction::transaction() const { Q_D(const Transaction); return d->m_transaction; } const MyMoneySplit& Transaction::split() const { Q_D(const Transaction); return d->m_split; } void Transaction::setBalance(const MyMoneyMoney& balance) { Q_D(Transaction); d->m_balance = balance; } const MyMoneyMoney& Transaction::balance() const { Q_D(const Transaction); return d->m_balance; } bool Transaction::paintRegisterCellSetup(QPainter *painter, QStyleOptionViewItem &option, const QModelIndex &index) { Q_D(Transaction); Q_UNUSED(painter) if (d->m_reducedIntensity) { option.palette.setColor(QPalette::Text, option.palette.color(QPalette::Disabled, QPalette::Text)); } if (d->m_selected) { option.state |= QStyle::State_Selected; } else { option.state &= ~QStyle::State_Selected; } if (d->m_focus) { option.state |= QStyle::State_HasFocus; } else { option.state &= ~QStyle::State_HasFocus; } if (option.widget && option.widget->hasFocus()) { option.palette.setCurrentColorGroup(QPalette::Active); } else { option.palette.setCurrentColorGroup(QPalette::Inactive); } if (index.column() == 0) { option.viewItemPosition = QStyleOptionViewItem::Beginning; } else if (index.column() == (int)eTransaction::Column::LastColumn - 1) { option.viewItemPosition = QStyleOptionViewItem::End; } else { option.viewItemPosition = QStyleOptionViewItem::Middle; } // do we need to switch to the error color? if (d->m_erroneous) { option.palette.setColor(QPalette::Text, KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } // do we need to switch to the negative balance color? if (index.column() == (int)eTransaction::Column::Balance) { bool showNegative = d->m_balance.isNegative(); if (d->m_account.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_balance.isZero()) showNegative = !showNegative; if (showNegative) option.palette.setColor(QPalette::Text, KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } return true; } void Transaction::registerCellText(QString& txt, int row, int col) { Qt::Alignment align; registerCellText(txt, align, row, col, 0); } void Transaction::paintRegisterCell(QPainter *painter, QStyleOptionViewItem &option, const QModelIndex &index) { Q_D(Transaction); painter->save(); if (paintRegisterCellSetup(painter, option, index)) { const QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const QWidget* widget = option.widget; // clear the mouse over state before painting the background option.state &= ~QStyle::State_MouseOver; // the background if (option.state & QStyle::State_Selected || option.state & QStyle::State_HasFocus) { // if this is not the first row of the transaction paint the previous rows // since the selection background is painted from the first row of the transaction if (index.row() > startRow()) { QStyleOptionViewItem optionSibling = option; QModelIndex previousRowItem = index.sibling(index.row() - 1, index.column()); optionSibling.rect = d->m_parent->visualRect(previousRowItem); paintRegisterCell(painter, optionSibling, previousRowItem); } // paint the selection background only from the first row on to the last row at once if (index.row() == startRow()) { QRect old = option.rect; int extraHeight = 0; if (d->m_inRegisterEdit) { // since, when editing a transaction inside the register (without the transaction form), // row heights can have various sizes (the memo row is larger than the rest) we have // to iterate over all the items of the transaction to compute the size of the selection rectangle // of course we start with the item after this one because it's size is already in the rectangle for (int i = startRow() + 1; i < startRow() + numRowsRegister(); ++i) { extraHeight += d->m_parent->visualRect(index.sibling(i, index.column())).height(); } } else { // we are not editing in the register so all rows have the same sizes just compute the extra height extraHeight = (numRowsRegister() - 1) * option.rect.height(); } option.rect.setBottom(option.rect.bottom() + extraHeight); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, widget); if (d->m_focus && index.column() == (int)eTransaction::Column::Detail) { option.state |= QStyle::State_HasFocus; style->drawPrimitive(QStyle::PE_FrameFocusRect, &option, painter, widget); } option.rect = old; } } else { if (d->m_alternate) { painter->fillRect(option.rect, option.palette.alternateBase()); } else { painter->fillRect(option.rect, option.palette.base()); } } // the text // construct the text for the cell QString txt; option.displayAlignment = Qt::AlignVCenter; - if (d->m_transaction != MyMoneyTransaction() && !d->m_inRegisterEdit) { + if (!d->m_transaction.id().isEmpty() && !d->m_inRegisterEdit) { registerCellText(txt, option.displayAlignment, index.row() - startRow(), index.column(), painter); } if (Qt::mightBeRichText(txt)) { QTextDocument document; // this should set the alignment of the html but it does not work so htmls will be left aligned document.setDefaultTextOption(QTextOption(option.displayAlignment)); document.setDocumentMargin(2); document.setHtml(txt); painter->translate(option.rect.topLeft()); QAbstractTextDocumentLayout::PaintContext ctx; ctx.palette = option.palette; // Highlighting text if item is selected if (d->m_selected) ctx.palette.setColor(QPalette::Text, option.palette.color(QPalette::HighlightedText)); document.documentLayout()->draw(painter, ctx); painter->translate(-option.rect.topLeft()); } else { // draw plain text properly aligned style->drawItemText(painter, option.rect.adjusted(2, 0, -2, 0), option.displayAlignment, option.palette, true, txt, d->m_selected ? QPalette::HighlightedText : QPalette::Text); } // draw the grid if it's needed if (KMyMoneySettings::showGrid()) { const int gridHint = style->styleHint(QStyle::SH_Table_GridLineColor, &option, widget); const QPen gridPen = QPen(QColor(static_cast(gridHint)), 0); QPen old = painter->pen(); painter->setPen(gridPen); if (index.row() == startRow()) painter->drawLine(option.rect.topLeft(), option.rect.topRight()); painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft()); painter->setPen(old); } // possible icons if (index.row() == startRow() && index.column() == (int)eTransaction::Column::Detail) { if (d->m_erroneous) { QPixmap attention; attention.loadFromData(attentionSign, sizeof(attentionSign), 0, 0); style->drawItemPixmap(painter, option.rect, Qt::AlignRight | Qt::AlignVCenter, attention); } } } painter->restore(); } bool Transaction::formCellText(QString& /* txt */, Qt::Alignment& /* align */, int /* row */, int /* col */, QPainter* /* painter */) { return false; } void Transaction::registerCellText(QString& /* txt */, Qt::Alignment& /* align */, int /* row */, int /* col */, QPainter* /* painter */) { } int Transaction::registerColWidth(int /* col */, const QFontMetrics& /* cellFontMetrics */) { return 0; } int Transaction::formRowHeight(int /*row*/) { Q_D(Transaction); if (d->m_formRowHeight < 0) { d->m_formRowHeight = formRowHeight(); } return d->m_formRowHeight; } int Transaction::formRowHeight() const { Q_D(const Transaction); if (d->m_formRowHeight < 0) { // determine the height of the objects in the table KMyMoneyDateInput dateInput; KMyMoneyCategory category(true, nullptr); return qMax(dateInput.sizeHint().height(), category.sizeHint().height()); } return d->m_formRowHeight; } void Transaction::setupForm(TransactionForm* form) { Q_D(Transaction); d->m_form = form; form->verticalHeader()->setUpdatesEnabled(false); form->horizontalHeader()->setUpdatesEnabled(false); form->setRowCount(numRowsForm()); form->setColumnCount(numColsForm()); // Force all cells to have some text (so that paintCell is called for each cell) for (int r = 0; r < numRowsForm(); ++r) { for (int c = 0; c < numColsForm(); ++c) { if (r == 0 && form->columnWidth(c) == 0) { form->setColumnWidth(c, 10); } } } form->horizontalHeader()->setUpdatesEnabled(true); form->verticalHeader()->setUpdatesEnabled(true); loadTab(form); } void Transaction::paintFormCell(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) { Q_D(Transaction); if (!d->m_form) return; QRect cellRect = option.rect; QRect textRect(cellRect); textRect.setWidth(textRect.width() - 2); textRect.setHeight(textRect.height() - 2); painter->setPen(option.palette.text().color()); QString txt; Qt::Alignment align = Qt::AlignVCenter; bool editField = formCellText(txt, align, index.row(), index.column(), painter); // if we have an editable field and don't currently edit the transaction // show the background in a different color if (editField && !d->m_inEdit) { painter->fillRect(textRect, option.palette.alternateBase()); } if (!d->m_inEdit) painter->drawText(textRect, align, txt); } void Transaction::setupPalette(const QPalette& palette, QMap& editWidgets) { QMap::iterator it_w; for (it_w = editWidgets.begin(); it_w != editWidgets.end(); ++it_w) { if (*it_w) { (*it_w)->setPalette(palette); } } } void Transaction::setupFormPalette(QMap& editWidgets) { Q_D(Transaction); QPalette palette = d->m_parent->palette(); palette.setColor(QPalette::Active, QPalette::Base, palette.color(QPalette::Active, QPalette::Base)); setupPalette(palette, editWidgets); } void Transaction::setupRegisterPalette(QMap& editWidgets) { Q_D(Transaction); // make sure, we're using the right palette QPalette palette = d->m_parent->palette(); // use the highlight color as background palette.setColor(QPalette::Active, QPalette::Background, palette.color(QPalette::Active, QPalette::Highlight)); setupPalette(palette, editWidgets); } QWidget* Transaction::focusWidget(QWidget* w) const { if (w) { while (w->focusProxy()) w = w->focusProxy(); } return w; } void Transaction::arrangeWidget(QTableWidget* tbl, int row, int col, QWidget* w) const { if (w) { tbl->setCellWidget(row, col, w); // remove the widget from the QTable's eventFilter so that all // events will be directed to the edit widget w->removeEventFilter(tbl); } } bool Transaction::haveNumberField() const { Q_D(const Transaction); auto rc = true; switch (d->m_account.accountType()) { case eMyMoney::Account::Type::Savings: case eMyMoney::Account::Type::Cash: case eMyMoney::Account::Type::Loan: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: rc = KMyMoneySettings::alwaysShowNrField(); break; case eMyMoney::Account::Type::Checkings: case eMyMoney::Account::Type::CreditCard: // the next case is used for the editor when the account // is unknown (eg. when creating new schedules) case eMyMoney::Account::Type::Unknown: break; default: rc = false; break; } return rc; } bool Transaction::maybeTip(const QPoint& cpos, int row, int col, QRect& r, QString& msg) { Q_D(Transaction); if (col != (int)eTransaction::Column::Detail) return false; if (!d->m_erroneous && d->m_transaction.splitCount() < 3) return false; // check for detail column in row 0 of the transaction for a possible // exclamation mark. m_startRow is based 0, whereas the row to obtain // the modelindex is based 1, so we need to add one here r = d->m_parent->visualRect(d->m_parent->model()->index(d->m_startRow + 1, col)); r.setBottom(r.bottom() + (numRowsRegister() - 1)*r.height()); if (r.contains(cpos) && d->m_erroneous) { if (d->m_transaction.splits().count() < 2) { msg = QString("%1").arg(i18n("Transaction is missing a category assignment.")); } else { const auto sec = MyMoneyFile::instance()->security(d->m_account.currencyId()); msg = QString("%1").arg(i18n("The transaction has a missing assignment of %1.", MyMoneyUtils::formatMoney(d->m_transaction.splitSum().abs(), d->m_account, sec))); } return true; } // check if the mouse cursor is located on row 1 of the transaction // and display the details of a split transaction if it is one if (row == 1 && r.contains(cpos) && d->m_transaction.splitCount() > 2) { auto file = MyMoneyFile::instance(); QString txt; const auto sec = file->security(d->m_transaction.commodity()); MyMoneyMoney factor(1, 1); if (!d->m_split.value().isNegative()) factor = -factor; foreach (const auto split, d->m_transaction.splits()) { if (split == d->m_split) continue; const MyMoneyAccount& acc = file->account(split.accountId()); auto category = file->accountToCategory(acc.id()); auto amount = MyMoneyUtils::formatMoney((split.value() * factor), acc, sec); txt += QString("%1%2").arg(category, amount); } msg = QString("%1
").arg(txt); return true; } return false; } QString Transaction::reconcileState(bool text) const { Q_D(const Transaction); auto txt = KMyMoneyUtils::reconcileStateToString(d->m_split.reconcileFlag(), text); if ((text == true) && (txt == i18nc("Unknown reconciliation state", "Unknown")) - && (d->m_transaction == MyMoneyTransaction())) + && (d->m_transaction.id().isEmpty())) txt.clear(); return txt; } void Transaction::startEditMode() { Q_D(Transaction); d->m_inEdit = true; // hide the original tabbar since the edit tabbar will be added if (auto form = dynamic_cast(d->m_form)) form->getTabBar()->setVisible(false); // only update the number of lines displayed if we edit inside the register if (d->m_inRegisterEdit) setNumRowsRegister(numRowsRegister(true)); } int Transaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } void Transaction::leaveEditMode() { Q_D(Transaction); // show the original tabbar since the edit tabbar was removed if (auto form = dynamic_cast(d->m_form)) form->getTabBar()->setVisible(true); // make sure we reset the row height of all the transaction's rows because it could have been changed during edit if (d->m_parent) { for (auto i = 0; i < numRowsRegister(); ++i) d->m_parent->setRowHeight(d->m_startRow + i, d->m_parent->rowHeightHint()); } d->m_inEdit = false; d->m_inRegisterEdit = false; setFocus(hasFocus(), true); } void Transaction::singleLineMemo(QString& txt, const MyMoneySplit& split) const { txt = split.memo(); // remove empty lines txt.replace("\n\n", "\n"); // replace '\n' with ", " txt.replace('\n', ", "); } int Transaction::rowHeightHint() const { Q_D(const Transaction); return d->m_inEdit ? formRowHeight() : RegisterItem::rowHeightHint(); } bool Transaction::matches(const RegisterFilter& filter) const { Q_D(const Transaction); // check if the state matches if (!transaction().id().isEmpty()) { switch (filter.state) { default: break; case eRegister::ItemState::Imported: if (!transaction().isImported()) return false; break; case eRegister::ItemState::Matched: if (!split().isMatched()) return false; break; case eRegister::ItemState::Erroneous: if (transaction().splitSum().isZero()) return false; break; case eRegister::ItemState::NotMarked: if (split().reconcileFlag() != eMyMoney::Split::State::NotReconciled) return false; break; case eRegister::ItemState::NotReconciled: if (split().reconcileFlag() != eMyMoney::Split::State::NotReconciled && split().reconcileFlag() != eMyMoney::Split::State::Cleared) return false; break; case eRegister::ItemState::Cleared: if (split().reconcileFlag() != eMyMoney::Split::State::Cleared) return false; break; case eRegister::ItemState::Scheduled: if (!isScheduled()) return false; break; } } // check if the text matches if (filter.text.isEmpty() || d->m_transaction.splitCount() == 0) return true; auto file = MyMoneyFile::instance(); foreach (const auto split, d->m_transaction.splits()) { // check if the text is contained in one of the fields // memo, number, payee, tag, account if (split.memo().contains(filter.text, Qt::CaseInsensitive) || split.number().contains(filter.text, Qt::CaseInsensitive)) return true; if (!split.payeeId().isEmpty()) { const MyMoneyPayee& payee = file->payee(split.payeeId()); if (payee.name().contains(filter.text, Qt::CaseInsensitive)) return true; } if (!split.tagIdList().isEmpty()) { const QList& t = split.tagIdList(); for (auto i = 0; i < t.count(); ++i) { if ((file->tag(t[i])).name().contains(filter.text, Qt::CaseInsensitive)) return true; } } const MyMoneyAccount& acc = file->account(split.accountId()); // search for account hierarchy if (filter.text.contains(MyMoneyFile::AccountSeparator)) { QStringList names; MyMoneyAccount current = acc; QString accountId; do { names.prepend(current.name()); accountId = current.parentAccountId(); current = file->account(accountId); } while (current.accountType() != eMyMoney::Account::Type::Unknown && !MyMoneyFile::instance()->isStandardAccount(accountId)); if (names.size() > 1 && names.join(MyMoneyFile::AccountSeparator).contains(filter.text, Qt::CaseInsensitive)) return true; } if (acc.name().contains(filter.text, Qt::CaseInsensitive)) return true; QString s(filter.text); s.replace(MyMoneyMoney::thousandSeparator(), QChar()); if (!s.isEmpty()) { // check if any of the value field matches if a value has been entered QString r = split.value().formatMoney(d->m_account.fraction(), false); if (r.contains(s, Qt::CaseInsensitive)) return true; const MyMoneyAccount& splitAcc = file->account(split.accountId()); r = split.shares().formatMoney(splitAcc.fraction(), false); if (r.contains(s, Qt::CaseInsensitive)) return true; } } return false; } void Transaction::setShowBalance(bool showBalance) { Q_D(Transaction); d->m_showBalance = showBalance; } bool Transaction::showRowInForm(int row) const { Q_UNUSED(row) return true; } void Transaction::setShowRowInForm(int row, bool show) { Q_UNUSED(row); Q_UNUSED(show) } void Transaction::setReducedIntensity(bool reduced) { Q_D(Transaction); d->m_reducedIntensity = reduced; } void Transaction::setVisible(bool visible) { Q_D(Transaction); if (visible != isVisible()) { RegisterItem::setVisible(visible); RegisterItem* p; Transaction* t; if (!visible) { // if we are hidden, we need to inform all previous transactions // about it so that they don't show the balance p = prevItem(); while (p) { if ((t = dynamic_cast(p))) { if (!t->d_func()->m_showBalance) break; t->d_func()->m_showBalance = false; } p = p->prevItem(); } } else { // if we are shown, we need to check if the next transaction // is visible and change the display of the balance p = this; do { p = p->nextItem(); t = dynamic_cast(p); } while (!t && p); // if the next transaction is visible or I am the last one if ((t && t->d_func()->m_showBalance) || !t) { d->m_showBalance = true; p = prevItem(); while (p && p->isVisible()) { if ((t = dynamic_cast(p))) { if (t->d_func()->m_showBalance) break; t->d_func()->m_showBalance = true; } p = p->prevItem(); } } } } }