diff --git a/kmymoney/dialogs/investtransactioneditor.cpp b/kmymoney/dialogs/investtransactioneditor.cpp index cc5c7b885..791ca4258 100644 --- a/kmymoney/dialogs/investtransactioneditor.cpp +++ b/kmymoney/dialogs/investtransactioneditor.cpp @@ -1,1229 +1,1229 @@ /* * 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); 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(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(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)); value->setPrecision(MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction())); + 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)); value->setPrecision(MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction())); + 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/kenterscheduledlg.cpp b/kmymoney/dialogs/kenterscheduledlg.cpp index 37566c08b..ad3b83bac 100644 --- a/kmymoney/dialogs/kenterscheduledlg.cpp +++ b/kmymoney/dialogs/kenterscheduledlg.cpp @@ -1,402 +1,406 @@ /* * Copyright 2007-2012 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 "kenterscheduledlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kenterscheduledlg.h" #include "tabbar.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "register.h" #include "transactionform.h" #include "transaction.h" #include "selectedtransactions.h" #include "transactioneditor.h" #include "kmymoneyutils.h" #include "kmymoneylineedit.h" #include "kmymoneydateinput.h" #include "knewaccountdlg.h" #include "knewinvestmentwizard.h" #include "mymoneyexception.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "widgetenums.h" using namespace Icons; class KEnterScheduleDlgPrivate { Q_DISABLE_COPY(KEnterScheduleDlgPrivate) public: KEnterScheduleDlgPrivate() : ui(new Ui::KEnterScheduleDlg), m_item(nullptr), m_showWarningOnce(true), m_extendedReturnCode(eDialogs::ScheduleResultCode::Cancel) { } ~KEnterScheduleDlgPrivate() { delete ui; } Ui::KEnterScheduleDlg *ui; MyMoneySchedule m_schedule; KMyMoneyRegister::Transaction* m_item; QWidgetList m_tabOrderWidgets; bool m_showWarningOnce; eDialogs::ScheduleResultCode m_extendedReturnCode; }; KEnterScheduleDlg::KEnterScheduleDlg(QWidget *parent, const MyMoneySchedule& schedule) : QDialog(parent), d_ptr(new KEnterScheduleDlgPrivate) { Q_D(KEnterScheduleDlg); // restore the last used dialog size - winId(); // needs to be called to create the QWindow KConfigGroup grp = KSharedConfig::openConfig()->group("KEnterScheduleDlg"); if (grp.isValid()) { KWindowConfig::restoreWindowSize(windowHandle(), grp); } // let the minimum size be 780x410 resize(QSize(780, 410).expandedTo(windowHandle() ? windowHandle()->size() : QSize())); + // position the dialog centered on the application (for some reason without + // a call to winId() the dialog is positioned in the upper left corner of + // the screen, but winId() crashes on MS-Windows ... + move(parent->pos() + QPoint(parent->width()/2, parent->height()/2) - QPoint(width()/2, height()/2)); + d->ui->setupUi(this); d->m_schedule = schedule; d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Enter; d->ui->buttonOk->setIcon(Icons::get(Icon::KeyEnter)); d->ui->buttonSkip->setIcon(Icons::get(Icon::MediaSeekForward)); KGuiItem::assign(d->ui->buttonCancel, KStandardGuiItem::cancel()); KGuiItem::assign(d->ui->buttonHelp, KStandardGuiItem::help()); d->ui->buttonIgnore->setHidden(true); d->ui->buttonSkip->setHidden(true); // make sure, we have a tabbar with the form KMyMoneyTransactionForm::TabBar* tabbar = d->ui->m_form->getTabBar(d->ui->m_form->parentWidget()); // we never need to see the register d->ui->m_register->hide(); // ... setup the form ... d->ui->m_form->setupForm(d->m_schedule.account()); // ... and the register ... d->ui->m_register->clear(); // ... now add the transaction to register and form ... MyMoneyTransaction t = transaction(); d->m_item = KMyMoneyRegister::Register::transactionFactory(d->ui->m_register, t, d->m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : d->m_schedule.transaction().splits().front(), 0); d->ui->m_register->selectItem(d->m_item); // show the account row d->m_item->setShowRowInForm(0, true); d->ui->m_form->slotSetTransaction(d->m_item); // no need to see the tabbar tabbar->hide(); // setup name and type d->ui->m_scheduleName->setText(d->m_schedule.name()); d->ui->m_type->setText(KMyMoneyUtils::scheduleTypeToString(d->m_schedule.type())); connect(d->ui->buttonHelp, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotShowHelp); connect(d->ui->buttonIgnore, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotIgnore); connect(d->ui->buttonSkip, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotSkip); } KEnterScheduleDlg::~KEnterScheduleDlg() { Q_D(KEnterScheduleDlg); // store the last used dialog size KConfigGroup grp = KSharedConfig::openConfig()->group("KEnterScheduleDlg"); if (grp.isValid()) { KWindowConfig::saveWindowSize(windowHandle(), grp); } delete d; } eDialogs::ScheduleResultCode KEnterScheduleDlg::resultCode() const { Q_D(const KEnterScheduleDlg); if (result() == QDialog::Accepted) return d->m_extendedReturnCode; return eDialogs::ScheduleResultCode::Cancel; } void KEnterScheduleDlg::showExtendedKeys(bool visible) { Q_D(KEnterScheduleDlg); d->ui->buttonIgnore->setVisible(visible); d->ui->buttonSkip->setVisible(visible); } void KEnterScheduleDlg::slotIgnore() { Q_D(KEnterScheduleDlg); d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Ignore; accept(); } void KEnterScheduleDlg::slotSkip() { Q_D(KEnterScheduleDlg); d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Skip; accept(); } MyMoneyTransaction KEnterScheduleDlg::transaction() { Q_D(KEnterScheduleDlg); auto t = d->m_schedule.transaction(); try { if (d->m_schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { KMyMoneyUtils::calculateAutoLoan(d->m_schedule, t, QMap()); } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Unable to load schedule details"), QString::fromLatin1(e.what())); } t.clearId(); t.setEntryDate(QDate()); return t; } QDate KEnterScheduleDlg::date(const QDate& _date) const { Q_D(const KEnterScheduleDlg); auto date(_date); return d->m_schedule.adjustedDate(date, d->m_schedule.weekendOption()); } void KEnterScheduleDlg::resizeEvent(QResizeEvent* ev) { Q_UNUSED(ev) Q_D(KEnterScheduleDlg); d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->ui->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); QDialog::resizeEvent(ev); } void KEnterScheduleDlg::slotSetupSize() { resize(width(), minimumSizeHint().height()); } int KEnterScheduleDlg::exec() { Q_D(KEnterScheduleDlg); if (d->m_showWarningOnce) { d->m_showWarningOnce = false; KMessageBox::information(parentWidget(), QString("") + i18n("

Please check that all the details in the following dialog are correct and press OK.

Editable data can be changed and can either be applied to just this occurrence or for all subsequent occurrences for this schedule. (You will be asked what you intend after pressing OK in the following dialog)

") + QString("
"), i18n("Enter scheduled transaction"), "EnterScheduleDlgInfo"); } // force the initial height to be as small as possible QTimer::singleShot(0, this, SLOT(slotSetupSize())); return QDialog::exec(); } TransactionEditor* KEnterScheduleDlg::startEdit() { Q_D(KEnterScheduleDlg); KMyMoneyRegister::SelectedTransactions list(d->ui->m_register); auto editor = d->m_item->createEditor(d->ui->m_form, list, QDate()); if (editor) { editor->setScheduleInfo(d->m_schedule.name()); editor->setPaymentMethod(d->m_schedule.paymentType()); } // 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_schedule.account())) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { connect(editor, &TransactionEditor::transactionDataSufficient, d->ui->buttonOk, &QWidget::setEnabled); connect(editor, &TransactionEditor::escapePressed, d->ui->buttonCancel, &QAbstractButton::animateClick); connect(editor, &TransactionEditor::returnPressed, d->ui->buttonOk, &QAbstractButton::animateClick); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // connect(editor, SIGNAL(finishEdit(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotLeaveEditMode(KMyMoneyRegister::SelectedTransactions))); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); eWidgets::eRegister::Action action = eWidgets::eRegister::Action::Withdrawal; switch (d->m_schedule.type()) { case eMyMoney::Schedule::Type::Transfer: action = eWidgets::eRegister::Action::Transfer; break; case eMyMoney::Schedule::Type::Deposit: action = eWidgets::eRegister::Action::Deposit; break; case eMyMoney::Schedule::Type::LoanPayment: switch (d->m_schedule.paymentType()) { case eMyMoney::Schedule::PaymentType::DirectDeposit: case eMyMoney::Schedule::PaymentType::ManualDeposit: action = eWidgets::eRegister::Action::Deposit; break; default: break; } break; default: break; } editor->setup(d->m_tabOrderWidgets, d->m_schedule.account(), action); MyMoneyTransaction t = d->m_schedule.transaction(); QString num = t.splits().first().number(); QWidget* w = editor->haveWidget("number"); if (d->m_schedule.paymentType() == eMyMoney::Schedule::PaymentType::WriteChecque) { num = KMyMoneyUtils::nextFreeCheckNumber(d->m_schedule.account()); d->m_schedule.account().setValue("lastNumberUsed", num); if (w) if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(num); } else { // if it's not a check, then we need to clear // a possibly assigned check number if (w) if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(QString()); } Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // editor->setup() leaves the tabbar as the last widget in the stack, but we // need it as first here. So we move it around. w = editor->haveWidget("tabbar"); if (w) { int idx = d->m_tabOrderWidgets.indexOf(w); if (idx != -1) { d->m_tabOrderWidgets.removeAt(idx); d->m_tabOrderWidgets.push_front(w); } } // don't forget our three buttons d->m_tabOrderWidgets.append(d->ui->buttonOk); d->m_tabOrderWidgets.append(d->ui->buttonCancel); d->m_tabOrderWidgets.append(d->ui->buttonHelp); for (auto i = 0; i < d->m_tabOrderWidgets.size(); ++i) { w = d->m_tabOrderWidgets.at(i); if (w) { w->installEventFilter(this); w->installEventFilter(editor); } } // 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(); focusWidget->setFocus(); // Make sure, we use the adjusted date if (auto dateEdit = dynamic_cast(editor->haveWidget("postdate"))) dateEdit->setDate(d->m_schedule.adjustedNextDueDate()); } return editor; } bool KEnterScheduleDlg::focusNextPrevChild(bool next) { Q_D(KEnterScheduleDlg); auto rc = false; auto w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", w->className()); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", w->className()); 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 = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", w->className()); w->setFocus(); rc = true; } } return rc; } void KEnterScheduleDlg::slotShowHelp() { KHelpClient::invokeHelp("details.schedules.entering"); } diff --git a/kmymoney/dialogs/stdtransactioneditor.cpp b/kmymoney/dialogs/stdtransactioneditor.cpp index fb499c476..987555b0d 100644 --- a/kmymoney/dialogs/stdtransactioneditor.cpp +++ b/kmymoney/dialogs/stdtransactioneditor.cpp @@ -1,1670 +1,1672 @@ /* * 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()) + if (category && !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()); + if (category && category->completion()) { + 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(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 44fcc1edc..8b09d6565 100644 --- a/kmymoney/dialogs/transactioneditor.cpp +++ b/kmymoney/dialogs/transactioneditor.cpp @@ -1,858 +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) { + if (k && (((k->modifiers() & Qt::KeyboardModifierMask) == 0) + || ((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/plugins/csv/import/bankingwizardpage.ui b/kmymoney/plugins/csv/import/bankingwizardpage.ui index 4a781dc77..9fae0e24c 100644 --- a/kmymoney/plugins/csv/import/bankingwizardpage.ui +++ b/kmymoney/plugins/csv/import/bankingwizardpage.ui @@ -1,480 +1,480 @@ BankingPage 0 0 681 253 0 0 Banking Wizard Page - + Qt::Horizontal 40 20 0 0 Please select the appropriate columns to use, corresponding to your data. Qt::AlignCenter true false Debit Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 16777215 16777215 Category Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing date field. 12 0 0 Select column containing category field. 12 false Credit Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing credit field. 12 Qt::Horizontal QSizePolicy::Minimum 6 0 0 0 false Select 'Debit/credit' if both columns exist, otherwise select 'Amount'. De&bit/credit false Date Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing amount field. 12 0 0 Select column containing debit field. 12 Payee/Description Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 0 0 Select column containing memo field. 12 Selection Clear selected memo column entries Clear 0 0 Select column containing payee or description field. 12 Memo Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing number field. false 12 Number Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Amount Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select 'Amount' if only one value column, otherwise select 'Debit/credit'. A&mount false 0 0 Qt::Horizontal 0 0 Select if your amount column has signs improperly set. Qt::LeftToRight Opposite signs 0 0 Clear all selected column entries Clear all Qt::Horizontal 40 20 m_numberCol m_dateCol m_payeeCol m_categoryCol m_memoCol m_clearMemoColumns m_radioAmount m_radioDebitCredit m_amountCol m_debitCol m_creditCol m_oppositeSigns m_clear diff --git a/kmymoney/plugins/csv/import/core/csvimportercore.cpp b/kmymoney/plugins/csv/import/core/csvimportercore.cpp index a2e5c0688..67c888c3e 100644 --- a/kmymoney/plugins/csv/import/core/csvimportercore.cpp +++ b/kmymoney/plugins/csv/import/core/csvimportercore.cpp @@ -1,1766 +1,1775 @@ /* * Copyright 2010 Allan Anderson * 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 "csvimportercore.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "csvutil.h" #include "convdate.h" #include "mymoneyenums.h" const QHash CSVImporterCore::m_profileConfPrefix { {Profile::Banking, QStringLiteral("Bank")}, {Profile::Investment, QStringLiteral("Invest")}, {Profile::CurrencyPrices, QStringLiteral("CPrices")}, {Profile::StockPrices, QStringLiteral("SPrices")} }; const QHash CSVImporterCore::m_colTypeConfName { {Column::Date, QStringLiteral("DateCol")}, {Column::Memo, QStringLiteral("MemoCol")}, {Column::Number, QStringLiteral("NumberCol")}, {Column::Payee, QStringLiteral("PayeeCol")}, {Column::Amount, QStringLiteral("AmountCol")}, {Column::Credit, QStringLiteral("CreditCol")}, {Column::Debit, QStringLiteral("DebitCol")}, {Column::Category, QStringLiteral("CategoryCol")}, {Column::Type, QStringLiteral("TypeCol")}, {Column::Price, QStringLiteral("PriceCol")}, {Column::Quantity, QStringLiteral("QuantityCol")}, {Column::Fee, QStringLiteral("FeeCol")}, {Column::Symbol, QStringLiteral("SymbolCol")}, {Column::Name, QStringLiteral("NameCol")}, }; const QHash CSVImporterCore::m_miscSettingsConfName { {ConfDirectory, QStringLiteral("Directory")}, {ConfEncoding, QStringLiteral("Encoding")}, {ConfDateFormat, QStringLiteral("DateFormat")}, {ConfFieldDelimiter, QStringLiteral("FieldDelimiter")}, {ConfTextDelimiter, QStringLiteral("TextDelimiter")}, {ConfDecimalSymbol, QStringLiteral("DecimalSymbol")}, {ConfStartLine, QStringLiteral("StartLine")}, {ConfTrailerLines, QStringLiteral("TrailerLines")}, {ConfOppositeSigns, QStringLiteral("OppositeSigns")}, {ConfFeeIsPercentage, QStringLiteral("FeeIsPercentage")}, {ConfFeeRate, QStringLiteral("FeeRate")}, {ConfMinFee, QStringLiteral("MinFee")}, {ConfSecurityName, QStringLiteral("SecurityName")}, {ConfSecuritySymbol, QStringLiteral("SecuritySymbol")}, {ConfCurrencySymbol, QStringLiteral("CurrencySymbol")}, {ConfPriceFraction, QStringLiteral("PriceFraction")}, {ConfDontAsk, QStringLiteral("DontAsk")}, {ConfHeight, QStringLiteral("Height")}, {ConfWidth, QStringLiteral("Width")} }; const QHash CSVImporterCore::m_transactionConfName { {eMyMoney::Transaction::Action::Buy, QStringLiteral("BuyParam")}, {eMyMoney::Transaction::Action::Sell, QStringLiteral("SellParam")}, {eMyMoney::Transaction::Action::ReinvestDividend, QStringLiteral("ReinvdivParam")}, {eMyMoney::Transaction::Action::CashDividend, QStringLiteral("DivXParam")}, {eMyMoney::Transaction::Action::Interest, QStringLiteral("IntIncParam")}, {eMyMoney::Transaction::Action::Shrsin, QStringLiteral("ShrsinParam")}, {eMyMoney::Transaction::Action::Shrsout, QStringLiteral("ShrsoutParam")} }; const QString CSVImporterCore::m_confProfileNames = QStringLiteral("ProfileNames"); const QString CSVImporterCore::m_confPriorName = QStringLiteral("Prior"); const QString CSVImporterCore::m_confMiscName = QStringLiteral("Misc"); CSVImporterCore::CSVImporterCore() : m_profile(0), m_isActionTypeValidated(false) { m_convertDate = new ConvertDate; m_file = new CSVFile; m_priceFractions << MyMoneyMoney(0.01) << MyMoneyMoney(0.1) << MyMoneyMoney::ONE << MyMoneyMoney(10) << MyMoneyMoney(100); validateConfigFile(); readMiscSettings(); } CSVImporterCore::~CSVImporterCore() { delete m_convertDate; delete m_file; } MyMoneyStatement CSVImporterCore::unattendedImport(const QString &filename, CSVProfile *profile) { MyMoneyStatement st; m_profile = profile; m_convertDate->setDateFormatIndex(m_profile->m_dateFormat); if (m_file->getInFileName(filename)) { m_file->readFile(m_profile); m_file->setupParser(m_profile); if (profile->m_decimalSymbol == DecimalSymbol::Auto) { auto columns = getNumericalColumns(); if (detectDecimalSymbols(columns) != -2) return st; } if (!createStatement(st)) st = MyMoneyStatement(); } return st; } KSharedConfigPtr CSVImporterCore::configFile() { return KSharedConfig::openConfig(QStringLiteral("kmymoney/csvimporterrc")); } void CSVImporterCore::profileFactory(const Profile type, const QString &name) { // delete current profile if (m_profile) { delete m_profile; m_profile = nullptr; } switch (type) { default: case Profile::Investment: m_profile = new InvestmentProfile; break; case Profile::Banking: m_profile = new BankingProfile; break; case Profile::CurrencyPrices: case Profile::StockPrices: m_profile = new PricesProfile(type); break; } m_profile->m_profileName = name; } void CSVImporterCore::readMiscSettings() { KConfigGroup miscGroup(configFile(), m_confMiscName); m_autodetect.clear(); m_autodetect.insert(AutoFieldDelimiter, miscGroup.readEntry(QStringLiteral("AutoFieldDelimiter"), true)); m_autodetect.insert(AutoDecimalSymbol, miscGroup.readEntry(QStringLiteral("AutoDecimalSymbol"), true)); m_autodetect.insert(AutoDateFormat, miscGroup.readEntry(QStringLiteral("AutoDateFormat"), true)); m_autodetect.insert(AutoAccountInvest, miscGroup.readEntry(QStringLiteral("AutoAccountInvest"), true)); m_autodetect.insert(AutoAccountBank, miscGroup.readEntry(QStringLiteral("AutoAccountBank"), true)); } void CSVImporterCore::validateConfigFile() { const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); if (!profileNamesGroup.exists()) { profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::CurrencyPrices), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::StockPrices), QStringList()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Banking), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Investment), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::CurrencyPrices), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::StockPrices), int()); profileNamesGroup.sync(); } KConfigGroup miscGroup(config, m_confMiscName); if (!miscGroup.exists()) { miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfHeight), "400"); miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfWidth), "800"); miscGroup.sync(); } QList confVer = miscGroup.readEntry("KMMVer", QList {0, 0, 0}); if (updateConfigFile(confVer)) // write kmmVer only if there were no errors miscGroup.writeEntry("KMMVer", confVer); } bool CSVImporterCore::updateConfigFile(QList &confVer) { bool ret = true; QList kmmVer = QList {5, 0, 0}; int kmmVersion = kmmVer.at(0) * 100 + kmmVer.at(1) * 10 + kmmVer.at(2); int confVersion = confVer.at(0) * 100 + confVer.at(1) * 10 + confVer.at(2); if (confVersion > kmmVersion) { KMessageBox::information(0, i18n("Version of your CSV config file is %1.%2.%3 and is newer than supported version %4.%5.%6. Expect troubles.", confVer.at(0), confVer.at(1), confVer.at(2), kmmVer.at(0), kmmVer.at(1), kmmVer.at(2))); ret = false; return ret; } else if (confVersion == kmmVersion) return true; confVer = kmmVer; const KSharedConfigPtr config = configFile(); QString configFilePath = config.constData()->name(); QFile::copy(configFilePath, configFilePath + QLatin1String(".bak")); KConfigGroup profileNamesGroup(config, m_confProfileNames); QStringList bankProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); QStringList investProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); QStringList invalidBankProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Banking), QStringList()); // get profiles that was marked invalid during last update QStringList invalidInvestProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Investment), QStringList()); QString bankPrefix = m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-'); QString investPrefix = m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-'); // for kmm < 5.0.0 change 'BankNames' to 'ProfileNames' and remove 'MainWindow' group if (confVersion < 500 && bankProfiles.isEmpty()) { KConfigGroup oldProfileNamesGroup(config, "BankProfiles"); bankProfiles = oldProfileNamesGroup.readEntry("BankNames", QStringList()); // profile names are under 'BankNames' entry for kmm < 5.0.0 bankPrefix = QLatin1String("Profiles-"); // needed to remove non-existent profiles in first run oldProfileNamesGroup.deleteGroup(); KConfigGroup oldMainWindowGroup(config, "MainWindow"); oldMainWindowGroup.deleteGroup(); KConfigGroup oldSecuritiesGroup(config, "Securities"); oldSecuritiesGroup.deleteGroup(); } bool firstTry = false; if (invalidBankProfiles.isEmpty() && invalidInvestProfiles.isEmpty()) // if there is no invalid profiles then this might be first update try firstTry = true; int invalidProfileResponse = QDialogButtonBox::No; for (auto profileName = bankProfiles.begin(); profileName != bankProfiles.end();) { KConfigGroup bankProfile(config, bankPrefix + *profileName); if (!bankProfile.exists() && !invalidBankProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = bankProfiles.erase(profileName); continue; } // for kmm < 5.0.0 remove 'FileType' and 'ProfileName' and assign them to either "Bank=" or "Invest=" if (confVersion < 500) { QString lastUsedDirectory; KConfigGroup oldBankProfile(config, QLatin1String("Profiles-") + *profileName); // if half of configuration is updated and the other one untouched this is needed QString oldProfileType = oldBankProfile.readEntry("FileType", QString()); KConfigGroup newProfile; if (oldProfileType == QLatin1String("Invest")) { oldBankProfile.deleteEntry("BrokerageParam"); oldBankProfile.writeEntry(m_colTypeConfName.value(Column::Type), oldBankProfile.readEntry("PayeeCol")); oldBankProfile.deleteEntry("PayeeCol"); oldBankProfile.deleteEntry("Filter"); oldBankProfile.deleteEntry("SecurityName"); lastUsedDirectory = oldBankProfile.readEntry("InvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-') + *profileName); investProfiles.append(*profileName); profileName = bankProfiles.erase(profileName); } else if (oldProfileType == QLatin1String("Banking")) { lastUsedDirectory = oldBankProfile.readEntry("CsvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-') + *profileName); ++profileName; } else { if (invalidProfileResponse != QDialogButtonBox::YesToAll && invalidProfileResponse != QDialogButtonBox::NoToAll) { if (!firstTry && !invalidBankProfiles.contains(*profileName)) { // if it isn't first update run and profile isn't on the list of invalid ones then don't bother ++profileName; continue; } invalidProfileResponse = KMessageBox::createKMessageBox(nullptr, new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::YesToAll | QDialogButtonBox::No | QDialogButtonBox::NoToAll), QMessageBox::Warning, i18n("
During update of %1
" "the profile type for %2 could not be recognized.
" "The profile cannot be used because of that.
" "Do you want to delete it?
", configFilePath, *profileName), QStringList(), QString(), nullptr, KMessageBox::Dangerous); } switch (invalidProfileResponse) { case QDialogButtonBox::YesToAll: case QDialogButtonBox::Yes: oldBankProfile.deleteGroup(); invalidBankProfiles.removeOne(*profileName); profileName = bankProfiles.erase(profileName); break; case QDialogButtonBox::NoToAll: case QDialogButtonBox::No: if (!invalidBankProfiles.contains(*profileName)) // on user request: don't delete profile but keep eye on it invalidBankProfiles.append(*profileName); ret = false; ++profileName; break; } continue; } oldBankProfile.deleteEntry("FileType"); oldBankProfile.deleteEntry("ProfileName"); oldBankProfile.deleteEntry("DebitFlag"); oldBankProfile.deleteEntry("InvDirectory"); oldBankProfile.deleteEntry("CsvDirectory"); oldBankProfile.sync(); oldBankProfile.copyTo(&newProfile); oldBankProfile.deleteGroup(); newProfile.writeEntry(m_miscSettingsConfName.value(ConfDirectory), lastUsedDirectory); newProfile.writeEntry(m_miscSettingsConfName.value(ConfEncoding), "106" /*UTF-8*/ ); // in 4.8 encoding wasn't supported well so set it to utf8 by default newProfile.sync(); } } for (auto profileName = investProfiles.begin(); profileName != investProfiles.end();) { KConfigGroup investProfile(config, investPrefix + *profileName); if (!investProfile.exists() && !invalidInvestProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = investProfiles.erase(profileName); continue; } ++profileName; } profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), bankProfiles); // update profile names as some of them might have been changed profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), investProfiles); if (invalidBankProfiles.isEmpty()) // if no invalid profiles then we don't need this variable anymore profileNamesGroup.deleteEntry("InvalidBank"); else profileNamesGroup.writeEntry("InvalidBank", invalidBankProfiles); if (invalidInvestProfiles.isEmpty()) profileNamesGroup.deleteEntry("InvalidInvest"); else profileNamesGroup.writeEntry("InvalidInvest", invalidInvestProfiles); if (ret) QFile::remove(configFilePath + ".bak"); // remove backup if all is ok return ret; } bool CSVImporterCore::profilesAction(const Profile type, const ProfileAction action, const QString &name, const QString &newname) { bool ret = false; const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); QString profileTypeStr = m_profileConfPrefix.value(type); QStringList profiles = profileNamesGroup.readEntry(profileTypeStr, QStringList()); KConfigGroup profileName(config, profileTypeStr + QLatin1Char('-') + name); switch (action) { case ProfileAction::UpdateLastUsed: profileNamesGroup.writeEntry(m_confPriorName + profileTypeStr, profiles.indexOf(name)); break; case ProfileAction::Add: if (!profiles.contains(newname)) { profiles.append(newname); ret = true; } break; case ProfileAction::Remove: { profiles.removeOne(name); profileName.deleteGroup(); profileName.sync(); ret = true; break; } case ProfileAction::Rename: { if (!newname.isEmpty() && name != newname) { int idx = profiles.indexOf(name); if (idx != -1) { profiles[idx] = newname; KConfigGroup newProfileName(config, profileTypeStr + QLatin1Char('-') + newname); if (profileName.exists() && !newProfileName.exists()) { profileName.copyTo(&newProfileName); profileName.deleteGroup(); profileName.sync(); newProfileName.sync(); ret = true; } } } break; } } profileNamesGroup.writeEntry(profileTypeStr, profiles); profileNamesGroup.sync(); return ret; } bool CSVImporterCore::validateDateFormat(const int col) { bool isOK = true; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem* item = m_file->m_model->item(row, col); QDate dat = m_convertDate->convertDate(item->text()); if (dat == QDate()) { isOK = false; break; } } return isOK; } bool CSVImporterCore::validateDecimalSymbols(const QList &columns) { bool isOK = true; foreach (const auto column, columns) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(column)); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem *item = m_file->m_model->item(row, column); QString rawNumber = item->text(); m_file->m_parse->possiblyReplaceSymbol(rawNumber); if (m_file->m_parse->invalidConversion() && !rawNumber.isEmpty()) { // empty strings are welcome isOK = false; break; } } } return isOK; } bool CSVImporterCore::validateCurrencies(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const InvestmentProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurities() { QSet onlySymbols; QSet onlyNames; sortSecurities(onlySymbols, onlyNames, m_mapSymbolName); if (!onlySymbols.isEmpty() || !onlyNames.isEmpty()) return false; return true; } eMyMoney::Transaction::Action CSVImporterCore::processActionTypeField(const InvestmentProfile *profile, const int row, const int col) { if (col == -1) return eMyMoney::Transaction::Action::None; QString type = m_file->m_model->item(row, col)->text(); QList actions; actions << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell << // first and second most frequent action eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::CashDividend << // we don't want "reinv-dividend" to be accidentally caught by "dividend" eMyMoney::Transaction::Action::Interest << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; foreach (const auto action, actions) { if (profile->m_transactionNames.value(action).contains(type, Qt::CaseInsensitive)) return action; } return eMyMoney::Transaction::Action::None; } validationResultE CSVImporterCore::validateActionType(MyMoneyStatement::Transaction &tr) { validationResultE ret = ValidActionType; QList validActionTypes = createValidActionTypes(tr); if (validActionTypes.isEmpty()) ret = InvalidActionValues; else if (!validActionTypes.contains(tr.m_eAction)) ret = NoActionType; return ret; } bool CSVImporterCore::calculateFee() { auto profile = dynamic_cast(m_profile); if (!profile) return false; if ((profile->m_feeRate.isEmpty() || // check whether feeRate... profile->m_colTypeNum.value(Column::Amount) == -1)) // ...and amount is in place return false; QString decimalSymbol; if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol detectedSymbol = detectDecimalSymbol(profile->m_colTypeNum.value(Column::Amount), QString()); if (detectedSymbol == DecimalSymbol::Auto) return false; m_file->m_parse->setDecimalSymbol(detectedSymbol); decimalSymbol = m_file->m_parse->decimalSymbol(detectedSymbol); } else decimalSymbol = m_file->m_parse->decimalSymbol(profile->m_decimalSymbol); MyMoneyMoney feePercent(m_file->m_parse->possiblyReplaceSymbol(profile->m_feeRate)); // convert 0.67% ... feePercent /= MyMoneyMoney(100); // ... to 0.0067 if (profile->m_minFee.isEmpty()) profile->m_minFee = QString::number(0.00, 'f', 2); MyMoneyMoney minFee(m_file->m_parse->possiblyReplaceSymbol(profile->m_minFee)); QList items; for (int row = 0; row < profile->m_startLine; ++row) // fill rows above with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); for (int row = profile->m_startLine; row <= profile->m_endLine; ++row) { QString txt, numbers; bool ok = false; numbers = txt = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Amount))->text(); numbers.remove(QRegularExpression(QStringLiteral("[,. ]"))).toInt(&ok); if (!ok) { // check if it's numerical string... items.append(new QStandardItem(QString())); continue; // ...and skip if not (TODO: allow currency symbols and IDs) } if (txt.startsWith(QLatin1Char('('))) { txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } txt = m_file->m_parse->possiblyReplaceSymbol(txt); MyMoneyMoney fee(txt); fee *= feePercent; if (fee < minFee) fee = minFee; txt.setNum(fee.toDouble(), 'f', 4); txt.replace(QLatin1Char('.'), decimalSymbol); //make sure decimal symbol is uniform in whole line items.append(new QStandardItem(txt)); } for (int row = profile->m_endLine + 1; row < m_file->m_rowCount; ++row) // fill rows below with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); int col = profile->m_colTypeNum.value(Column::Fee, -1); if (col == -1) { // fee column isn't present m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else if (col >= m_file->m_columnCount) { // column number must have been stored in profile m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else { // fee column is present and has been recalculated m_file->m_model->removeColumn(m_file->m_columnCount - 1); m_file->m_model->appendColumn(items); } profile->m_colTypeNum[Column::Fee] = m_file->m_columnCount - 1; return true; } DecimalSymbol CSVImporterCore::detectDecimalSymbol(const int col, const QString &exclude) { DecimalSymbol detectedSymbol = DecimalSymbol::Auto; QString pattern; QRegularExpression re("^[\\(+-]?\\d+[\\)]?$"); // matches '0' ; '+12' ; '-345' ; '(6789)' bool dotIsDecimalSeparator = false; bool commaIsDecimalSeparator = false; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString txt = m_file->m_model->item(row, col)->text(); if (txt.isEmpty()) // nothing to process, so go to next row continue; int dotPos = txt.lastIndexOf(QLatin1Char('.')); // get last positions of decimal/thousand separator... int commaPos = txt.lastIndexOf(QLatin1Char(',')); // ...to be able to determine which one is the last if (dotPos != -1 && commaPos != -1) { if (dotPos > commaPos && commaIsDecimalSeparator == false) // following case 1,234.56 dotIsDecimalSeparator = true; else if (dotPos < commaPos && dotIsDecimalSeparator == false) // following case 1.234,56 commaIsDecimalSeparator = true; else // following case 1.234,56 and somewhere earlier there was 1,234.56 so unresolvable conflict return detectedSymbol; } else if (dotPos != -1) { // following case 1.23 if (dotIsDecimalSeparator) // it's already know that dotIsDecimalSeparator continue; if (!commaIsDecimalSeparator) // if there is no conflict with comma as decimal separator dotIsDecimalSeparator = true; else { if (txt.count(QLatin1Char('.')) > 1) // following case 1.234.567 so OK continue; else if (txt.length() - 4 == dotPos) // following case 1.234 and somewhere earlier there was 1.234,56 so OK continue; else // following case 1.23 and somewhere earlier there was 1,23 so unresolvable conflict return detectedSymbol; } } else if (commaPos != -1) { // following case 1,23 if (commaIsDecimalSeparator) // it's already know that commaIsDecimalSeparator continue; else if (!dotIsDecimalSeparator) // if there is no conflict with dot as decimal separator commaIsDecimalSeparator = true; else { if (txt.count(QLatin1Char(',')) > 1) // following case 1,234,567 so OK continue; else if (txt.length() - 4 == commaPos) // following case 1,234 and somewhere earlier there was 1,234.56 so OK continue; else // following case 1,23 and somewhere earlier there was 1.23 so unresolvable conflict return detectedSymbol; } } else { // following case 123 if (pattern.isEmpty()) { } txt.remove(QRegularExpression(QLatin1String("[ ") + QRegularExpression::escape(exclude) + QLatin1String("]"))); QRegularExpressionMatch match = re.match(txt); if (match.hasMatch()) // if string is pure numerical then go forward... continue; else // ...if not then it's non-numerical garbage return detectedSymbol; } } if (dotIsDecimalSeparator) detectedSymbol = DecimalSymbol::Dot; else if (commaIsDecimalSeparator) detectedSymbol = DecimalSymbol::Comma; else { // whole column was empty, but we don't want to fail so take OS's decimal symbol if (QLocale().decimalPoint() == QLatin1Char('.')) detectedSymbol = DecimalSymbol::Dot; else detectedSymbol = DecimalSymbol::Comma; } return detectedSymbol; } int CSVImporterCore::detectDecimalSymbols(const QList &columns) { int ret = -2; // get list of used currencies to remove them from col QList accounts; MyMoneyFile *file = MyMoneyFile::instance(); file->accountList(accounts); QList accountTypes; accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; QSet currencySymbols; foreach (const auto account, accounts) { if (accountTypes.contains(account.accountType())) { // account must actually have currency property currencySymbols.insert(account.currencyId()); // add currency id currencySymbols.insert(file->currency(account.currencyId()).tradingSymbol()); // add currency symbol } } QString filteredCurrencies = QStringList(currencySymbols.values()).join(""); QString pattern = QString::fromLatin1("%1%2").arg(QLocale().currencySymbol()).arg(filteredCurrencies); foreach (const auto column, columns) { DecimalSymbol detectedSymbol = detectDecimalSymbol(column, pattern); if (detectedSymbol == DecimalSymbol::Auto) { ret = column; return ret; } m_decimalSymbolIndexMap.insert(column, detectedSymbol); } return ret; } QList CSVImporterCore::findAccounts(const QList &accountTypes, const QString &statementHeader) { MyMoneyFile* file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList filteredTypes; QList filteredAccounts; QRegularExpression filterOutChars(QStringLiteral("[-., ]")); foreach (const auto account, accountList) { if (accountTypes.contains(account.accountType()) && !(account).isClosed()) filteredTypes.append(account); } // filter out accounts whose names aren't in statements header foreach (const auto account, filteredTypes) { QString txt = account.name(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } // if filtering returned more results, filter out accounts whose numbers aren't in statements header if (filteredAccounts.count() > 1) { for (auto account = filteredAccounts.begin(); account != filteredAccounts.end();) { QString txt = (*account).number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) { ++account; continue; } if (statementHeader.contains(txt, Qt::CaseInsensitive)) ++account; else account = filteredAccounts.erase(account); } } // if filtering returned more results, filter out accounts whose numbers are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccNumber = filteredAccounts.at(0).number(); auto secondAccNumber = filteredAccounts.at(i).number(); if (firstAccNumber.length() > secondAccNumber.length()) { filteredAccounts.removeAt(i); } else if (firstAccNumber.length() < secondAccNumber.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering returned more results, filter out accounts whose names are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccName = filteredAccounts.at(0).name(); auto secondAccName = filteredAccounts.at(i).name(); if (firstAccName.length() > secondAccName.length()) { filteredAccounts.removeAt(i); } else if (firstAccName.length() < secondAccName.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering by name and number didn't return nothing, then try filtering by number only if (filteredAccounts.isEmpty()) { foreach (const auto account, filteredTypes) { QString txt = account.number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } } return filteredAccounts; } bool CSVImporterCore::detectAccount(MyMoneyStatement &st) { QString statementHeader; for (int row = 0; row < m_profile->m_startLine; ++row) // concatenate header for better search for (int col = 0; col < m_file->m_columnCount; ++col) statementHeader.append(m_file->m_model->item(row, col)->text()); statementHeader.remove(QRegularExpression(QStringLiteral("[-., ]"))); QList accounts; QList accountTypes; switch(m_profile->type()) { default: case Profile::Banking: accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; accounts = findAccounts(accountTypes, statementHeader); break; case Profile::Investment: accountTypes << eMyMoney::Account::Type::Investment; // take investment accounts... accounts = findAccounts(accountTypes, statementHeader); //...and search them in statement header break; } if (accounts.count() == 1) { // set account in statement, if it was the only one match st.m_strAccountName = accounts.first().name(); st.m_strAccountNumber = accounts.first().number(); st.m_accountId = accounts.first().id(); switch (accounts.first().accountType()) { case eMyMoney::Account::Type::Checkings: st.m_eType = eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: st.m_eType = eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: st.m_eType = eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: st.m_eType = eMyMoney::Statement::Type::CreditCard; break; default: st.m_eType = eMyMoney::Statement::Type::None; } return true; } return false; } bool CSVImporterCore::processBankRow(MyMoneyStatement &st, const BankingProfile *profile, const int row) { MyMoneyStatement::Transaction tr; QString memo; QString txt; + if (!profile) + return false; + // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process number field col = profile->m_colTypeNum.value(Column::Number, -1); if (col != -1) tr.m_strNumber = m_file->m_model->item(row, col)->text(); // process payee field col = profile->m_colTypeNum.value(Column::Payee, -1); if (col != -1) tr.m_strPayee = m_file->m_model->item(row, col)->text(); // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); if (col != -1 && profile->m_oppositeSigns) // change signs to opposite if requested by user tr.m_amount *= MyMoneyMoney(-1); // process credit/debit field if (profile->m_colTypeNum.value(Column::Credit, -1) != -1 && profile->m_colTypeNum.value(Column::Debit, -1) != -1) { QString credit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Credit))->text(); QString debit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Debit))->text(); tr.m_amount = processCreditDebit(credit, debit); if (!credit.isEmpty() && !debit.isEmpty()) return false; } MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = -s1.m_amount; // process category field col = profile->m_colTypeNum.value(Column::Category, -1); if (col != -1) { txt = m_file->m_model->item(row, col)->text(); QString accountId = MyMoneyFile::instance()->checkCategory(txt, s1.m_amount, s2.m_amount); if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = txt; tr.m_listSplits.append(s2); } } // calculate hash txt.clear(); for (int i = 0; i < m_file->m_columnCount; ++i) txt.append(m_file->m_model->item(row, i)->text()); QString hashBase = QString::fromLatin1("%1-%2") .arg(tr.m_datePosted.toString(Qt::ISODate)) .arg(MyMoneyTransaction::hash(txt)); QString hash; for (uchar idx = 0; idx < 0xFF; ++idx) { // assuming threre will be no more than 256 transactions with the same hashBase hash = QString::fromLatin1("%1-%2").arg(hashBase).arg(idx); QSet::const_iterator it = m_hashSet.constFind(hash); if (it == m_hashSet.constEnd()) break; } m_hashSet.insert(hash); tr.m_strBankID = hash; st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processInvestRow(MyMoneyStatement &st, const InvestmentProfile *profile, const int row) { MyMoneyStatement::Transaction tr; + if (!profile) + return false; + QString memo; QString txt; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process quantity field col = profile->m_colTypeNum.value(Column::Quantity, -1); tr.m_shares = processQuantityField(profile, row, col); // process price field col = profile->m_colTypeNum.value(Column::Price, -1); tr.m_price = processPriceField(profile, row, col); // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); // process type field col = profile->m_colTypeNum.value(Column::Type, -1); tr.m_eAction = processActionTypeField(profile, row, col); if (!m_isActionTypeValidated && col != -1 && // if action type wasn't validated in wizard then... validateActionType(tr) != ValidActionType) // ...check if price, amount, quantity is appropriate return false; // process fee field col = profile->m_colTypeNum.value(Column::Fee, -1); if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol decimalSymbol = m_decimalSymbolIndexMap.value(col); m_file->m_parse->setDecimalSymbol(decimalSymbol); } txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); if (txt.isEmpty()) tr.m_fees = MyMoneyMoney(); else { MyMoneyMoney fee(m_file->m_parse->possiblyReplaceSymbol(txt)); if (profile->m_feeIsPercentage && profile->m_feeRate.isEmpty()) // fee is percent fee *= tr.m_amount / MyMoneyMoney(100); // as percentage fee.abs(); tr.m_fees = fee; } } // process symbol and name field col = profile->m_colTypeNum.value(Column::Symbol, -1); if (col != -1) tr.m_strSymbol = m_file->m_model->item(row, col)->text(); col = profile->m_colTypeNum.value(Column::Name, -1); if (col != -1 && tr.m_strSymbol.isEmpty()) { // case in which symbol field is empty txt = m_file->m_model->item(row, col)->text(); tr.m_strSymbol = m_mapSymbolName.key(txt); // it's all about getting the right symbol } else if (!profile->m_securitySymbol.isEmpty()) tr.m_strSymbol = profile->m_securitySymbol; else if (tr.m_strSymbol.isEmpty()) return false; tr.m_strSecurity = m_mapSymbolName.value(tr.m_strSymbol); // take name from prepared names to avoid potential name mismatch // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; tr.m_strInterestCategory.clear(); // no special category tr.m_strBrokerageAccount.clear(); // no brokerage account auto-detection MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_amount = -s1.m_amount; s2.m_accountId = MyMoneyFile::instance()->checkCategory(tr.m_strInterestCategory, s1.m_amount, s2.m_amount); // deduct fees from amount if (tr.m_eAction == eMyMoney::Transaction::Action::CashDividend || tr.m_eAction == eMyMoney::Transaction::Action::Sell || tr.m_eAction == eMyMoney::Transaction::Action::Interest) tr.m_amount -= tr.m_fees; else if (tr.m_eAction == eMyMoney::Transaction::Action::Buy) { if (tr.m_amount.isPositive()) tr.m_amount = -tr.m_amount; //if broker doesn't use minus sings for buy transactions, set it manually here tr.m_amount -= tr.m_fees; } else if (tr.m_eAction == eMyMoney::Transaction::Action::None) tr.m_listSplits.append(s2); st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processPriceRow(MyMoneyStatement &st, const PricesProfile *profile, const int row) { MyMoneyStatement::Price pr; + if (!profile) + return false; + // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); pr.m_date = processDateField(row, col); if (pr.m_date == QDate()) return false; // process price field col = profile->m_colTypeNum.value(Column::Price, -1); pr.m_amount = processPriceField(profile, row, col); switch (profile->type()) { case Profile::CurrencyPrices: if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; pr.m_strSecurity = profile->m_securitySymbol; pr.m_strCurrency = profile->m_currencySymbol; break; case Profile::StockPrices: if (profile->m_securityName.isEmpty()) return false; pr.m_strSecurity = profile->m_securityName; break; default: return false; } pr.m_sourceName = profile->m_profileName; st.m_listPrices.append(pr); // Add price to the statement return true; } QDate CSVImporterCore::processDateField(const int row, const int col) { QDate date; if (col != -1) { QString txt = m_file->m_model->item(row, col)->text(); date = m_convertDate->convertDate(txt); // Date column } return date; } MyMoneyMoney CSVImporterCore::processCreditDebit(QString &credit, QString &debit) { MyMoneyMoney amount; if (m_profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(m_profile->m_colTypeNum.value(Column::Credit)); if (credit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers credit.remove(QRegularExpression(QStringLiteral("[()]"))); credit.prepend(QLatin1Char('-')); } if (debit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers debit.remove(QRegularExpression(QStringLiteral("[()]"))); debit.prepend(QLatin1Char('-')); } if (!credit.isEmpty() && !debit.isEmpty()) { // we do not expect both fields to be non-zero if (MyMoneyMoney(credit).isZero()) credit = QString(); if (MyMoneyMoney(debit).isZero()) debit = QString(); } if (!debit.startsWith(QLatin1Char('-')) && !debit.isEmpty()) // ensure debit field is negative debit.prepend(QLatin1Char('-')); if (!credit.isEmpty() && debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(credit)); else if (credit.isEmpty() && !debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(debit)); else if (!credit.isEmpty() && !debit.isEmpty()) { // both fields are non-empty and non-zero so let user decide return amount; } else amount = MyMoneyMoney(); // both fields are empty and zero so set amount to zero return amount; } MyMoneyMoney CSVImporterCore::processQuantityField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney shares; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); txt.remove(QRegularExpression(QStringLiteral("-+"))); // remove unwanted sings in quantity if (!txt.isEmpty()) shares = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return shares; } MyMoneyMoney CSVImporterCore::processAmountField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney amount; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } if (!txt.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return amount; } MyMoneyMoney CSVImporterCore::processPriceField(const InvestmentProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } MyMoneyMoney CSVImporterCore::processPriceField(const PricesProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } QList CSVImporterCore::createValidActionTypes(MyMoneyStatement::Transaction &tr) { QList validActionTypes; if (tr.m_shares.isPositive() && tr.m_price.isPositive() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell; else if (tr.m_shares.isZero() && tr.m_price.isZero() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::CashDividend << eMyMoney::Transaction::Action::Interest; else if (tr.m_shares.isPositive() && tr.m_price.isZero() && tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; return validActionTypes; } bool CSVImporterCore::sortSecurities(QSet& onlySymbols, QSet& onlyNames, QMap& mapSymbolName) { QList securityList = MyMoneyFile::instance()->securityList(); int symbolCol = m_profile->m_colTypeNum.value(Column::Symbol, -1); int nameCol = m_profile->m_colTypeNum.value(Column::Name, -1); // sort by availability of symbol and name for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString symbol; QString name; if (symbolCol != -1) symbol = m_file->m_model->item(row, symbolCol)->text().trimmed(); if (nameCol != -1) name = m_file->m_model->item(row, nameCol)->text().trimmed(); if (!symbol.isEmpty() && !name.isEmpty()) mapSymbolName.insert(symbol, name); else if (!symbol.isEmpty()) onlySymbols.insert(symbol); else if (!name.isEmpty()) onlyNames.insert(name); else return false; } // try to find names for symbols for (QSet::iterator symbol = onlySymbols.begin(); symbol != onlySymbols.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*symbol).compare(sec.tradingSymbol(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by symbol } if (filteredSecurities.count() == 1) { // single security matched by the symbol so... mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the symbol // TODO: Ask user which security should we match to mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); } else // no security matched, so leave it as unknown ++symbol; } // try to find symbols for names for (QSet::iterator name = onlyNames.begin(); name != onlyNames.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*name).compare(sec.name(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by name } if (filteredSecurities.count() == 1) { // single security matched by the name so... mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlyNames.erase(name); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the name // TODO: Ask user which security should we match to mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlySymbols.erase(name); } else // no security matched, so leave it as unknown ++name; } return true; } void CSVImporterCore::setupFieldDecimalSymbol(int col) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(col)); } QList CSVImporterCore::getNumericalColumns() { QList columns; switch(m_profile->type()) { case Profile::Banking: if (m_profile->m_colTypeNum.value(Column::Amount, -1) != -1) { columns << m_profile->m_colTypeNum.value(Column::Amount); } else { columns << m_profile->m_colTypeNum.value(Column::Debit); columns << m_profile->m_colTypeNum.value(Column::Credit); } break; case Profile::Investment: columns << m_profile->m_colTypeNum.value(Column::Amount); columns << m_profile->m_colTypeNum.value(Column::Price); columns << m_profile->m_colTypeNum.value(Column::Quantity); if (m_profile->m_colTypeNum.value(Column::Fee, -1) != -1) columns << m_profile->m_colTypeNum.value(Column::Fee); break; case Profile::CurrencyPrices: case Profile::StockPrices: columns << m_profile->m_colTypeNum.value(Column::Price); break; default: break; } return columns; } bool CSVImporterCore::createStatement(MyMoneyStatement &st) { switch (m_profile->type()) { case Profile::Banking: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (m_autodetect.value(AutoAccountBank)) detectAccount(st); m_hashSet.clear(); BankingProfile *profile = dynamic_cast(m_profile); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processBankRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } return true; break; } case Profile::Investment: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::Investment; if (m_autodetect.value(AutoAccountInvest)) detectAccount(st); auto profile = dynamic_cast(m_profile); if ((m_profile->m_colTypeNum.value(Column::Fee, -1) == -1 || m_profile->m_colTypeNum.value(Column::Fee, -1) >= m_file->m_columnCount) && profile && !profile->m_feeRate.isEmpty()) // fee column has not been calculated so do it now calculateFee(); if (profile) { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processInvestRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; break; } default: case Profile::CurrencyPrices: case Profile::StockPrices: { if (!st.m_listPrices.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (auto profile = dynamic_cast(m_profile)) { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processPriceRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; } } return true; } void CSVProfile::readSettings(const KConfigGroup &profilesGroup) { m_lastUsedDirectory = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), QString()); m_startLine = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), 0); m_trailerLines = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), 0); m_encodingMIBEnum = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), 106 /* UTF-8 */); m_dateFormat = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)DateFormat::YearMonthDay)); m_textDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)TextDelimiter::DoubleQuote)); m_fieldDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)FieldDelimiter::Auto)); m_decimalSymbol = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)DecimalSymbol::Auto)); initColNumType(); } void CSVProfile::writeSettings(KConfigGroup &profilesGroup) { QFileInfo fileInfo (m_lastUsedDirectory); if (fileInfo.isFile()) m_lastUsedDirectory = fileInfo.absolutePath(); if (m_lastUsedDirectory.startsWith(QDir::homePath())) // replace /home/user with ~/ for brevity m_lastUsedDirectory.replace(0, QDir::homePath().length(), QLatin1Char('~')); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), m_lastUsedDirectory); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), m_encodingMIBEnum); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)m_dateFormat); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)m_fieldDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)m_textDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)m_decimalSymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), m_startLine); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), m_trailerLines); } bool BankingProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Payee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), -1); m_colTypeNum[Column::Number] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Debit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), -1); m_colTypeNum[Column::Credit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), -1); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Category] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_oppositeSigns = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), false); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); CSVProfile::readSettings(profilesGroup); return exists; } void BankingProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), m_oppositeSigns); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), m_colTypeNum.value(Column::Payee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), m_colTypeNum.value(Column::Number)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), m_colTypeNum.value(Column::Debit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), m_colTypeNum.value(Column::Credit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), m_colTypeNum.value(Column::Category)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool InvestmentProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_transactionNames[eMyMoney::Transaction::Action::Buy] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), QString(i18nc("Type of operation as in financial statement", "buy")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Sell] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), QString(i18nc("Type of operation as in financial statement", "sell,repurchase")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::ReinvestDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), QString(i18nc("Type of operation as in financial statement", "reinvest,reinv,re-inv")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::CashDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), QString(i18nc("Type of operation as in financial statement", "dividend")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Interest] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), QString(i18nc("Type of operation as in financial statement", "interest,income")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsin] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), QString(i18nc("Type of operation as in financial statement", "add,stock dividend,divd reinv,transfer in,re-registration in,journal entry")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsout] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), QString(i18nc("Type of operation as in financial statement", "remove")).split(',', QString::SkipEmptyParts)); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Type] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), -1); //use for type col. m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_colTypeNum[Column::Quantity] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Name] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), -1); m_colTypeNum[Column::Fee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), -1); m_colTypeNum[Column::Symbol] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_feeIsPercentage = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), false); m_feeRate = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), QString()); m_minFee = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), QString()); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); CSVProfile::readSettings(profilesGroup); return exists; } void InvestmentProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), m_transactionNames.value(eMyMoney::Transaction::Action::Buy)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), m_transactionNames.value(eMyMoney::Transaction::Action::Sell)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), m_transactionNames.value(eMyMoney::Transaction::Action::ReinvestDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), m_transactionNames.value(eMyMoney::Transaction::Action::CashDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), m_transactionNames.value(eMyMoney::Transaction::Action::Interest)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsin)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsout)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), m_feeIsPercentage); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), m_feeRate); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), m_minFee); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), m_colTypeNum.value(Column::Type)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), m_colTypeNum.value(Column::Quantity)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), m_colTypeNum.value(Column::Symbol)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), m_colTypeNum.value(Column::Name)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), m_colTypeNum.value(Column::Fee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool PricesProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_currencySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); CSVProfile::readSettings(profilesGroup); return exists; } void PricesProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), m_currencySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.config()->sync(); } CSVFile::CSVFile() : m_columnCount(0), m_rowCount(0) { m_parse = new Parse; m_model = new QStandardItemModel; } CSVFile::~CSVFile() { delete m_parse; delete m_model; } void CSVFile::getStartEndRow(CSVProfile *profile) { profile->m_endLine = m_rowCount - 1; if (profile->m_endLine > profile->m_trailerLines) profile->m_endLine -= profile->m_trailerLines; if (profile->m_startLine > profile->m_endLine) // Don't allow m_startLine > m_endLine profile->m_startLine = profile->m_endLine; } void CSVFile::getColumnCount(CSVProfile *profile, const QStringList &rows) { if (rows.isEmpty()) return; QVector delimiterIndexes; if (profile->m_fieldDelimiter == FieldDelimiter::Auto) delimiterIndexes = QVector{FieldDelimiter::Comma, FieldDelimiter::Semicolon, FieldDelimiter::Colon, FieldDelimiter::Tab}; // include all delimiters to test or ... else delimiterIndexes = QVector{profile->m_fieldDelimiter}; // ... only the one specified QList totalDelimiterCount({0, 0, 0, 0}); // Total in file for each delimiter QList thisDelimiterCount({0, 0, 0, 0}); // Total in this line for each delimiter int colCount = 0; // Total delimiters in this line FieldDelimiter possibleDelimiter = FieldDelimiter::Comma; m_columnCount = 0; foreach (const auto row, rows) { foreach(const auto delimiterIndex, delimiterIndexes) { m_parse->setFieldDelimiter(delimiterIndex); colCount = m_parse->parseLine(row).count(); // parse each line using each delimiter if (colCount > thisDelimiterCount.at((int)delimiterIndex)) thisDelimiterCount[(int)delimiterIndex] = colCount; if (thisDelimiterCount[(int)delimiterIndex] > m_columnCount) m_columnCount = thisDelimiterCount.at((int)delimiterIndex); totalDelimiterCount[(int)delimiterIndex] += colCount; if (totalDelimiterCount.at((int)delimiterIndex) > totalDelimiterCount.at((int)possibleDelimiter)) possibleDelimiter = delimiterIndex; } } if (delimiterIndexes.count() != 1) // if purpose was to autodetect... profile->m_fieldDelimiter = possibleDelimiter; // ... then change field delimiter m_parse->setFieldDelimiter(profile->m_fieldDelimiter); // restore original field delimiter } bool CSVFile::getInFileName(QString inFileName) { QFileInfo fileInfo; if (!inFileName.isEmpty()) { if (inFileName.startsWith(QLatin1Char('~'))) inFileName.replace(0, 1, QDir::homePath()); fileInfo = QFileInfo(inFileName); if (fileInfo.isFile()) { // if it is file... if (fileInfo.exists()) { // ...and exists... m_inFileName = inFileName; // ...then set as valid filename return true; // ...and return success... } else { // ...but if not... fileInfo.setFile(fileInfo.absolutePath()); //...then set start directory to directory of that file... if (!fileInfo.exists()) //...and if it doesn't exist too... fileInfo.setFile(QDir::homePath()); //...then set start directory to home path } } else if (fileInfo.isDir()) { if (fileInfo.exists()) fileInfo = QFileInfo(inFileName); else fileInfo.setFile(QDir::homePath()); } } else fileInfo = QFileInfo(QDir::homePath()); QPointer dialog = new QFileDialog(nullptr, QString(), fileInfo.absoluteFilePath(), i18n("CSV Files (*.csv)")); dialog->setFileMode(QFileDialog::ExistingFile); QUrl url; if (dialog->exec() == QDialog::Accepted) url = dialog->selectedUrls().first(); delete dialog; if (url.isEmpty()) { m_inFileName.clear(); return false; } else m_inFileName = url.toDisplayString(QUrl::PreferLocalFile); return true; } void CSVFile::setupParser(CSVProfile *profile) { if (profile->m_decimalSymbol != DecimalSymbol::Auto) m_parse->setDecimalSymbol(profile->m_decimalSymbol); m_parse->setFieldDelimiter(profile->m_fieldDelimiter); m_parse->setTextDelimiter(profile->m_textDelimiter); } void CSVFile::readFile(CSVProfile *profile) { QFile inFile(m_inFileName); if (!inFile.exists()) return; inFile.open(QIODevice::ReadOnly); QTextStream inStream(&inFile); QTextCodec* codec = QTextCodec::codecForMib(profile->m_encodingMIBEnum); inStream.setCodec(codec); QString buf = inStream.readAll(); inFile.close(); m_parse->setTextDelimiter(profile->m_textDelimiter); QStringList rows = m_parse->parseFile(buf); // parse the buffer m_rowCount = m_parse->lastLine(); // won't work without above line getColumnCount(profile, rows); getStartEndRow(profile); // prepare model from rows having rowCount and columnCount m_model->clear(); for (int i = 0; i < m_rowCount; ++i) { QList itemList; QStringList columns = m_parse->parseLine(rows.takeFirst()); // take instead of read from rows to preserve memory for (int j = 0; j < m_columnCount; ++j) itemList.append(new QStandardItem(columns.value(j, QString()))); m_model->appendRow(itemList); } } diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp index d88b104f1..0581757b8 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp @@ -1,2693 +1,2693 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 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 "mymoneygncreader.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5,10,0) #include #endif // ---------------------------------------------------------------------------- // KDE Includes #ifndef _GNCFILEANON #include #include #endif #include // ---------------------------------------------------------------------------- // Third party Includes // ------------------------------------------------------------Box21---------------- // Project Includes #include #include "mymoneystoragemgr.h" #ifndef _GNCFILEANON #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "kgncimportoptionsdlg.h" #include "kgncpricesourcedlg.h" #include "keditscheduledlg.h" #include "kmymoneyedit.h" #include "kmymoneymoneyvalidator.h" #define TRY try #define CATCH catch (const MyMoneyException &) #define PASS catch (const MyMoneyException &) { throw; } #else #include "mymoneymoney.h" #include // #define i18n QObject::tr #define TRY #define CATCH #define PASS #define MYMONEYEXCEPTION QString #define MyMoneyException QString #define PACKAGE "KMyMoney" #endif // _GNCFILEANON #include "mymoneyenums.h" using namespace eMyMoney; // init static variables double MyMoneyGncReader::m_fileHideFactor = 0.0; double GncObject::m_moneyHideFactor; // user options void MyMoneyGncReader::setOptions() { #ifndef _GNCFILEANON KGncImportOptionsDlg dlg; // display the dialog to allow the user to set own options if (dlg.exec()) { // set users input options m_dropSuspectSchedules = dlg.scheduleOption(); m_investmentOption = dlg.investmentOption(); m_useFinanceQuote = dlg.quoteOption(); m_useTxNotes = dlg.txNotesOption(); m_decoder = dlg.decodeOption(); gncdebug = dlg.generalDebugOption(); xmldebug = dlg.xmlDebugOption(); bAnonymize = dlg.anonymizeOption(); } else { // user declined, so set some sensible defaults m_dropSuspectSchedules = false; // investment option - 0, create investment a/c per stock a/c, 1 = single new investment account, 2 = prompt for each stock // option 2 doesn't really work too well at present m_investmentOption = 0; m_useFinanceQuote = false; m_useTxNotes = false; m_decoder = 0; gncdebug = false; // general debug messages xmldebug = false; // xml trace bAnonymize = false; // anonymize input } // no dialog option for the following; it will set base currency, and print actual XML data developerDebug = false; // set your fave currency here to save getting that enormous dialog each time you run a test // especially if you have to scroll down to USD... if (developerDebug) m_storage->setValue("kmm-baseCurrency", "GBP"); #endif // _GNCFILEANON } GncObject::GncObject() : pMain(0), m_subElementList(0), m_subElementListCount(0), m_dataElementList(0), m_dataElementListCount(0), m_dataPtr(0), m_state(0), m_anonClassList(0), m_anonClass(0) { } // Check that the current element is of a version we are coded for void GncObject::checkVersion(const QString& elName, const QXmlAttributes& elAttrs, const map_elementVersions& map) { TRY { if (map.contains(elName)) { // if it's not in the map, there's nothing to check if (!map[elName].contains(elAttrs.value("version"))) throw MYMONEYEXCEPTION(QString::fromLatin1("%1 : Sorry. This importer cannot handle version %2 of element %3").arg(Q_FUNC_INFO, elAttrs.value("version"), elName)); } return ; } PASS } // Check if this element is in the current object's sub element list GncObject *GncObject::isSubElement(const QString& elName, const QXmlAttributes& elAttrs) { TRY { uint i; GncObject *next = 0; for (i = 0; i < m_subElementListCount; i++) { if (elName == m_subElementList[i]) { m_state = i; next = startSubEl(); // go create the sub object if (next != 0) { next->initiate(elName, elAttrs); // initialize it next->m_elementName = elName; // save it's name so we can identify the end } break; } } return (next); } PASS } // Check if this element is in the current object's data element list bool GncObject::isDataElement(const QString &elName, const QXmlAttributes& elAttrs) { TRY { uint i; for (i = 0; i < m_dataElementListCount; i++) { if (elName == m_dataElementList[i]) { m_state = i; dataEl(elAttrs); // go set the pointer so the data can be stored return (true); } } m_dataPtr = 0; // we don't need this, so make sure we don't store extraneous data return (false); } PASS } // return the variable string, decoded if required QString GncObject::var(int i) const { /* This code was needed because the Qt3 XML reader apparently did not process the encoding parameter in the m_decoder == 0 ? m_v[i] : pMain->m_decoder->toUnicode(m_v[i].toUtf8())); } const QString GncObject::getKvpValue(const QString& key, const QString& type) const { QList::const_iterator it; // first check for exact match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key() == key) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } // then for partial match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key().contains(key)) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } return (QString()); } void GncObject::adjustHideFactor() { #if QT_VERSION >= QT_VERSION_CHECK(5,10,0) m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * QRandomGenerator::system()->generate() / (RAND_MAX + 1.0))) / 100.0; #else - m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * rand() / (RAND_MAX + 1.0))) / 100.0; + m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * qrand() / (RAND_MAX + 1.0))) / 100.0; #endif } // data anonymizer QString GncObject::hide(QString data, unsigned int anonClass) { TRY { if (!pMain->bAnonymize) return (data); // no anonymizing required // counters used to generate names for anonymizer static int nextAccount; static int nextEquity; static int nextPayee; static int nextSched; static QMap anonPayees; // to check for duplicate payee names static QMap anonStocks; // for reference to equities QString result(data); QMap::const_iterator it; MyMoneyMoney in, mresult; switch (anonClass) { case ASIS: // this is not personal data break; case SUPPRESS: // this is personal and is not essential result = ""; break; case NXTACC: // generate account name result = ki18n("Account%1").subs(++nextAccount, -6).toString(); break; case NXTEQU: // generate/return an equity name it = anonStocks.constFind(data); if (it == anonStocks.constEnd()) { result = ki18n("Stock%1").subs(++nextEquity, -6).toString(); anonStocks.insert(data, result); } else { result = (*it); } break; case NXTPAY: // generate/return a payee name it = anonPayees.constFind(data); if (it == anonPayees.constEnd()) { result = ki18n("Payee%1").subs(++nextPayee, -6).toString(); anonPayees.insert(data, result); } else { result = (*it); } break; case NXTSCHD: // generate a schedule name result = ki18n("Schedule%1").subs(++nextSched, -6).toString(); break; case MONEY1: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); // spurious gnucash data - causes a crash sometimes mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); result = mresult.toString(); break; case MONEY2: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); mresult.setThousandSeparator(' '); result = mresult.formatMoney("", 2); break; } return (result); } PASS } // dump current object data values // only called if gncdebug set void GncObject::debugDump() { uint i; qDebug() << "Object" << m_elementName; for (i = 0; i < m_dataElementListCount; i++) { qDebug() << m_dataElementList[i] << "=" << m_v[i]; } } //***************************************************************** GncFile::GncFile() { static const QString subEls[] = {"gnc:book", "gnc:count-data", "gnc:commodity", "price", "gnc:account", "gnc:transaction", "gnc:template-transactions", "gnc:schedxaction" }; m_subElementList = subEls; m_subElementListCount = END_FILE_SELS; m_dataElementListCount = 0; m_processingTemplates = false; m_bookFound = false; } GncFile::~GncFile() {} GncObject *GncFile::startSubEl() { TRY { if (pMain->xmldebug) qDebug("File start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case BOOK: if (m_bookFound) MYMONEYEXCEPTION(QString::fromLatin1("This version of the importer cannot handle multi-book files.")); m_bookFound = true; break; case COUNT: next = new GncCountData; break; case CMDTY: next = new GncCommodity; break; case PRICE: next = new GncPrice; break; case ACCT: // accounts within the template section are ignored if (!m_processingTemplates) next = new GncAccount; break; case TX: next = new GncTransaction(m_processingTemplates); break; case TEMPLATES: m_processingTemplates = true; break; case SCHEDULES: m_processingTemplates = false; next = new GncSchedule; break; default: throw MYMONEYEXCEPTION_CSTRING("GncFile rcvd invalid state"); } return (next); } PASS } void GncFile::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("File end subel"); if (!m_processingTemplates) delete subObj; // template txs must be saved awaiting schedules m_dataPtr = 0; return ; } //****************************************** GncDate ********************************************* GncDate::GncDate() { m_subElementListCount = 0; static const QString dEls[] = {"ts:date", "gdate"}; m_dataElementList = dEls; m_dataElementListCount = END_Date_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncDate::~GncDate() {} //*************************************GncCmdtySpec*************************************** GncCmdtySpec::GncCmdtySpec() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id"}; m_dataElementList = dEls; m_dataElementListCount = END_CmdtySpec_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCmdtySpec::~GncCmdtySpec() {} QString GncCmdtySpec::hide(QString data, unsigned int) { // hide equity names, but not currency names unsigned int newClass = ASIS; switch (m_state) { case CMDTYID: if (!isCurrency()) newClass = NXTEQU; } return (GncObject::hide(data, newClass)); } //************* GncKvp******************************************** GncKvp::GncKvp() { m_subElementListCount = END_Kvp_SELS; static const QString subEls[] = {"slot"}; // kvp's may be nested m_subElementList = subEls; m_dataElementListCount = END_Kvp_DELS; static const QString dataEls[] = {"slot:key", "slot:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncKvp::~GncKvp() {} void GncKvp::dataEl(const QXmlAttributes& elAttrs) { switch (m_state) { case VALUE: m_kvpType = elAttrs.value("type"); } m_dataPtr = &(m_v[m_state]); if (key().contains("formula")) { m_anonClass = MONEY2; } else { m_anonClass = ASIS; } return ; } GncObject *GncKvp::startSubEl() { if (pMain->xmldebug) qDebug("Kvp start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION_CSTRING("GncKvp rcvd invalid m_state "); } return (next); } PASS } void GncKvp::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Kvp end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //*********************************GncLot********************************************* GncLot::GncLot() { m_subElementListCount = 0; m_dataElementListCount = 0; } GncLot::~GncLot() {} //*********************************GncCountData*************************************** GncCountData::GncCountData() { m_subElementListCount = 0; m_dataElementListCount = 0; m_v.append(QString()); // only 1 data item } GncCountData::~GncCountData() {} void GncCountData::initiate(const QString&, const QXmlAttributes& elAttrs) { m_countType = elAttrs.value("cd:type"); m_dataPtr = &(m_v[0]); return ; } void GncCountData::terminate() { int i = m_v[0].toInt(); if (m_countType == "commodity") { pMain->setGncCommodityCount(i); return ; } if (m_countType == "account") { pMain->setGncAccountCount(i); return ; } if (m_countType == "transaction") { pMain->setGncTransactionCount(i); return ; } if (m_countType == "schedxaction") { pMain->setGncScheduleCount(i); return ; } if (i != 0) { if (m_countType == "budget") pMain->setBudgetsFound(true); else if (m_countType.left(7) == "gnc:Gnc") pMain->setSmallBusinessFound(true); else if (pMain->xmldebug) qDebug() << "Unknown count type" << m_countType; } return ; } //*********************************GncCommodity*************************************** GncCommodity::GncCommodity() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id", "cmdty:name", "cmdty:fraction"}; m_dataElementList = dEls; m_dataElementListCount = END_Commodity_DELS; static const unsigned int anonClasses[] = {ASIS, NXTEQU, SUPPRESS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCommodity::~GncCommodity() {} void GncCommodity::terminate() { TRY { pMain->convertCommodity(this); return ; } PASS } //************* GncPrice******************************************** GncPrice::GncPrice() { static const QString subEls[] = {"price:commodity", "price:currency", "price:time"}; m_subElementList = subEls; m_subElementListCount = END_Price_SELS; m_dataElementListCount = END_Price_DELS; static const QString dataEls[] = {"price:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; m_vpCurrency = 0; m_vpPriceDate = 0; } GncPrice::~GncPrice() { delete m_vpCommodity; delete m_vpCurrency; delete m_vpPriceDate; } GncObject *GncPrice::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case CURR: next = new GncCmdtySpec; break; case PRICEDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION_CSTRING("GncPrice rcvd invalid m_state"); } return (next); } PASS } void GncPrice::endSubEl(GncObject *subObj) { TRY { switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case CURR: m_vpCurrency = static_cast(subObj); break; case PRICEDATE: m_vpPriceDate = static_cast(subObj); break; default: throw MYMONEYEXCEPTION_CSTRING("GncPrice rcvd invalid m_state"); } return; } PASS } void GncPrice::terminate() { TRY { pMain->convertPrice(this); return ; } PASS } //************* GncAccount******************************************** GncAccount::GncAccount() { m_subElementListCount = END_Account_SELS; static const QString subEls[] = {"act:commodity", "slot", "act:lots"}; m_subElementList = subEls; m_dataElementListCount = END_Account_DELS; static const QString dataEls[] = {"act:id", "act:name", "act:description", "act:type", "act:parent" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, NXTACC, SUPPRESS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; } GncAccount::~GncAccount() { delete m_vpCommodity; } GncObject *GncAccount::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Account start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case KVP: next = new GncKvp; break; case LOTS: next = new GncLot(); pMain->setLotsFound(true); // we don't handle lots; just set flag to report break; default: throw MYMONEYEXCEPTION_CSTRING("GncAccount rcvd invalid m_state"); } return (next); } PASS } void GncAccount::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Account end subel"); switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncAccount::terminate() { TRY { pMain->convertAccount(this); return ; } PASS } //************* GncTransaction******************************************** GncTransaction::GncTransaction(bool processingTemplates) { m_subElementListCount = END_Transaction_SELS; static const QString subEls[] = {"trn:currency", "trn:date-posted", "trn:date-entered", "trn:split", "slot" }; m_subElementList = subEls; m_dataElementListCount = END_Transaction_DELS; static const QString dataEls[] = {"trn:id", "trn:num", "trn:description"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, NXTPAY}; m_anonClassList = anonClasses; adjustHideFactor(); m_template = processingTemplates; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCurrency = 0; m_vpDateEntered = m_vpDatePosted = 0; } GncTransaction::~GncTransaction() { delete m_vpCurrency; delete m_vpDatePosted; delete m_vpDateEntered; } GncObject *GncTransaction::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Transaction start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CURRCY: next = new GncCmdtySpec; break; case POSTED: case ENTERED: next = new GncDate; break; case SPLIT: if (isTemplate()) { next = new GncTemplateSplit; } else { next = new GncSplit; } break; case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION_CSTRING("GncTransaction rcvd invalid m_state"); } return (next); } PASS } void GncTransaction::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Transaction end subel"); switch (m_state) { case CURRCY: m_vpCurrency = static_cast(subObj); break; case POSTED: m_vpDatePosted = static_cast(subObj); break; case ENTERED: m_vpDateEntered = static_cast(subObj); break; case SPLIT: m_splitList.append(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncTransaction::terminate() { TRY { if (isTemplate()) { pMain->saveTemplateTransaction(this); } else { pMain->convertTransaction(this); } return ; } PASS } //************* GncSplit******************************************** GncSplit::GncSplit() { m_subElementListCount = END_Split_SELS; static const QString subEls[] = {"split:reconcile-date"}; m_subElementList = subEls; m_dataElementListCount = END_Split_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpDateReconciled = 0; } GncSplit::~GncSplit() { delete m_vpDateReconciled; } GncObject *GncSplit::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case RECDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION_CSTRING("GncTemplateSplit rcvd invalid m_state "); } return (next); } PASS } void GncSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Split end subel"); switch (m_state) { case RECDATE: m_vpDateReconciled = static_cast(subObj); break; } return ; } //************* GncTemplateSplit******************************************** GncTemplateSplit::GncTemplateSplit() { m_subElementListCount = END_TemplateSplit_SELS; static const QString subEls[] = {"slot"}; m_subElementList = subEls; m_dataElementListCount = END_TemplateSplit_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncTemplateSplit::~GncTemplateSplit() {} GncObject *GncTemplateSplit::startSubEl() { if (pMain->xmldebug) qDebug("TemplateSplit start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION_CSTRING("GncTemplateSplit rcvd invalid m_state"); } return (next); } PASS } void GncTemplateSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("TemplateSplit end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //************* GncSchedule******************************************** GncSchedule::GncSchedule() { m_subElementListCount = END_Schedule_SELS; static const QString subEls[] = {"sx:start", "sx:last", "sx:end", "gnc:freqspec", "gnc:recurrence", "sx:deferredInstance"}; m_subElementList = subEls; m_dataElementListCount = END_Schedule_DELS; static const QString dataEls[] = {"sx:name", "sx:enabled", "sx:autoCreate", "sx:autoCreateNotify", "sx:autoCreateDays", "sx:advanceCreateDays", "sx:advanceRemindDays", "sx:instanceCount", "sx:num-occur", "sx:rem-occur", "sx:templ-acct" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {NXTSCHD, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpStartDate = m_vpLastDate = m_vpEndDate = 0; m_vpFreqSpec = 0; m_vpRecurrence.clear(); m_vpSchedDef = 0; } GncSchedule::~GncSchedule() { delete m_vpStartDate; delete m_vpLastDate; delete m_vpEndDate; delete m_vpFreqSpec; delete m_vpSchedDef; } GncObject *GncSchedule::startSubEl() { if (pMain->xmldebug) qDebug("Schedule start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case STARTDATE: case LASTDATE: case ENDDATE: next = new GncDate; break; case FREQ: next = new GncFreqSpec; break; case RECURRENCE: next = new GncRecurrence; break; case DEFINST: next = new GncSchedDef; break; default: throw MYMONEYEXCEPTION_CSTRING("GncSchedule rcvd invalid m_state"); } return (next); } PASS } void GncSchedule::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Schedule end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; case LASTDATE: m_vpLastDate = static_cast(subObj); break; case ENDDATE: m_vpEndDate = static_cast(subObj); break; case FREQ: m_vpFreqSpec = static_cast(subObj); break; case RECURRENCE: m_vpRecurrence.append(static_cast(subObj)); break; case DEFINST: m_vpSchedDef = static_cast(subObj); break; } return ; } void GncSchedule::terminate() { TRY { pMain->convertSchedule(this); return ; } PASS } //************* GncFreqSpec******************************************** GncFreqSpec::GncFreqSpec() { m_subElementListCount = END_FreqSpec_SELS; static const QString subEls[] = {"gnc:freqspec"}; m_subElementList = subEls; m_dataElementListCount = END_FreqSpec_DELS; static const QString dataEls[] = {"fs:ui_type", "fs:monthly", "fs:daily", "fs:weekly", "fs:interval", "fs:offset", "fs:day" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS }; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncFreqSpec::~GncFreqSpec() {} GncObject *GncFreqSpec::startSubEl() { TRY { if (pMain->xmldebug) qDebug("FreqSpec start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case COMPO: next = new GncFreqSpec; break; default: throw MYMONEYEXCEPTION_CSTRING("GncFreqSpec rcvd invalid m_state"); } return (next); } PASS } void GncFreqSpec::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("FreqSpec end subel"); switch (m_state) { case COMPO: m_fsList.append(subObj); break; } m_dataPtr = 0; return ; } void GncFreqSpec::terminate() { pMain->convertFreqSpec(this); return ; } //************* GncRecurrence******************************************** GncRecurrence::GncRecurrence() : m_vpStartDate(0) { m_subElementListCount = END_Recurrence_SELS; static const QString subEls[] = {"recurrence:start"}; m_subElementList = subEls; m_dataElementListCount = END_Recurrence_DELS; static const QString dataEls[] = {"recurrence:mult", "recurrence:period_type"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncRecurrence::~GncRecurrence() { delete m_vpStartDate; } GncObject *GncRecurrence::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Recurrence start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case STARTDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION_CSTRING("GncRecurrence rcvd invalid m_state"); } return (next); } PASS } void GncRecurrence::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Recurrence end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; } m_dataPtr = 0; return ; } void GncRecurrence::terminate() { pMain->convertRecurrence(this); return ; } QString GncRecurrence::getFrequency() const { // This function converts a gnucash 2.2 recurrence specification into it's previous equivalent // This will all need re-writing when MTE finishes the schedule re-write if (periodType() == "once") return("once"); if ((periodType() == "day") && (mult() == "1")) return("daily"); if (periodType() == "week") { if (mult() == "1") return ("weekly"); if (mult() == "2") return ("bi_weekly"); if (mult() == "4") return ("four-weekly"); } if (periodType() == "month") { if (mult() == "1") return ("monthly"); if (mult() == "2") return ("two-monthly"); if (mult() == "3") return ("quarterly"); if (mult() == "4") return ("tri_annually"); if (mult() == "6") return ("semi_yearly"); if (mult() == "12") return ("yearly"); if (mult() == "24") return ("two-yearly"); } return ("unknown"); } //************* GncSchedDef******************************************** GncSchedDef::GncSchedDef() { // process ing for this sub-object is undefined at the present time m_subElementListCount = 0; m_dataElementListCount = 0; } GncSchedDef::~GncSchedDef() {} /************************************************************************************************ XML Reader ************************************************************************************************/ XmlReader::XmlReader(MyMoneyGncReader *pM) : m_source(0), m_reader(0), m_co(0), pMain(pM), m_headerFound(false) { } void XmlReader::processFile(QIODevice* pDevice) { m_source = new QXmlInputSource(pDevice); // set up the Qt XML reader m_reader = new QXmlSimpleReader; m_reader->setContentHandler(this); // go read the file if (!m_reader->parse(m_source)) throw MYMONEYEXCEPTION(QString::fromLatin1("Input file cannot be parsed; may be corrupt\n%1").arg(errorString())); delete m_reader; delete m_source; return ; } // XML handling routines bool XmlReader::startDocument() { m_co = new GncFile; // create initial object, push to stack , pass it the 'main' pointer m_os.push(m_co); m_co->setPm(pMain); m_headerFound = false; #ifdef _GNCFILEANON pMain->oStream << ""; lastType = -1; indentCount = 0; #endif // _GNCFILEANON return (true); } bool XmlReader::startElement(const QString&, const QString&, const QString& elName , const QXmlAttributes& elAttrs) { try { if (pMain->gncdebug) qDebug() << "XML start -" << elName; #ifdef _GNCFILEANON int i; QString spaces; // anonymizer - write data if (elName == "gnc:book" || elName == "gnc:count-data" || elName == "book:id") lastType = -1; pMain->oStream << endl; switch (lastType) { case 0: indentCount += 2; // tricky fall through here case 2: spaces.fill(' ', indentCount); pMain->oStream << spaces.toLatin1(); break; } pMain->oStream << '<' << elName; for (i = 0; i < elAttrs.count(); ++i) { pMain->oStream << ' ' << elAttrs.qName(i) << '=' << '"' << elAttrs.value(i) << '"'; } pMain->oStream << '>'; lastType = 0; #else if ((!m_headerFound) && (elName != "gnc-v2")) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid header for file. Should be 'gnc-v2'")); m_headerFound = true; #endif // _GNCFILEANON m_co->checkVersion(elName, elAttrs, pMain->m_versionList); // check if this is a sub object element; if so, push stack and initialize GncObject *temp = m_co->isSubElement(elName, elAttrs); if (temp != 0) { m_os.push(temp); m_co = m_os.top(); m_co->setVersion(elAttrs.value("version")); m_co->setPm(pMain); // pass the 'main' pointer to the sub object // return true; // removed, as we hit a return true anyway } #if 0 // check for a data element if (m_co->isDataElement(elName, elAttrs)) return (true); #endif else { // reduced the above to m_co->isDataElement(elName, elAttrs); } } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", QString::fromLatin1(e.what())), PACKAGE); qWarning("%s", e.what()); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return true; // to keep compiler happy } bool XmlReader::endElement(const QString&, const QString&, const QString&elName) { try { if (pMain->xmldebug) qDebug() << "XML end -" << elName; #ifdef _GNCFILEANON QString spaces; switch (lastType) { case 2: indentCount -= 2; spaces.fill(' ', indentCount); pMain->oStream << endl << spaces.toLatin1(); break; } pMain->oStream << "' ; lastType = 2; #endif // _GNCFILEANON m_co->resetDataPtr(); // so we don't get extraneous data loaded into the variables if (elName == m_co->getElName()) { // check if this is the end of the current object if (pMain->gncdebug) m_co->debugDump(); // dump the object data (temp) // call the terminate routine, pop the stack, and advise the parent that it's done m_co->terminate(); GncObject *temp = m_co; m_os.pop(); m_co = m_os.top(); m_co->endSubEl(temp); } return (true); } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", QString::fromLatin1(e.what())), PACKAGE); qWarning("%s", e.what()); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return (true); // to keep compiler happy } bool XmlReader::characters(const QString &data) { if (pMain->xmldebug) qDebug("XML Data received - %d bytes", data.length()); QString pData = data.trimmed(); // data may contain line feeds and indentation spaces if (!pData.isEmpty()) { if (pMain->developerDebug) qDebug() << "XML Data -" << pData; m_co->storeData(pData); //go store it #ifdef _GNCFILEANON QString anonData = m_co->getData(); if (anonData.isEmpty()) anonData = pData; // there must be a Qt standard way of doing the following but I can't ... find it anonData.replace('<', "<"); anonData.replace('>', ">"); anonData.replace('&', "&"); pMain->oStream << anonData; // write original data lastType = 1; #endif // _GNCFILEANON } return (true); } bool XmlReader::endDocument() { #ifdef _GNCFILEANON pMain->oStream << endl << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; #endif // _GNCFILEANON return (true); } /******************************************************************************************* Main class for this module Controls overall operation of the importer ********************************************************************************************/ //***************** Constructor *********************** MyMoneyGncReader::MyMoneyGncReader() : m_dropSuspectSchedules(0), m_investmentOption(0), m_useFinanceQuote(0), m_useTxNotes(0), gncdebug(0), xmldebug(0), bAnonymize(0), developerDebug(0), m_xr(0), m_progressCallback(0), m_ccCount(0), m_orCount(0), m_scCount(0), m_potentialTransfer(0), m_suspectSchedule(false) { #ifndef _GNCFILEANON m_storage = 0; #endif // _GNCFILEANON // to hold gnucash count data (only used for progress bar) m_gncCommodityCount = m_gncAccountCount = m_gncTransactionCount = m_gncScheduleCount = 0; m_smallBusinessFound = m_budgetsFound = m_lotsFound = false; m_commodityCount = m_priceCount = m_accountCount = m_transactionCount = m_templateCount = m_scheduleCount = 0; m_decoder = 0; // build a list of valid versions static const QString versionList[] = {"gnc:book 2.0.0", "gnc:commodity 2.0.0", "gnc:pricedb 1", "gnc:account 2.0.0", "gnc:transaction 2.0.0", "gnc:schedxaction 1.0.0", "gnc:schedxaction 2.0.0", // for gnucash 2.2 onward "gnc:freqspec 1.0.0", "zzz" // zzz = stopper }; unsigned int i; for (i = 0; versionList[i] != "zzz"; ++i) m_versionList[versionList[i].section(' ', 0, 0)].append(versionList[i].section(' ', 1, 1)); } //***************** Destructor ************************* MyMoneyGncReader::~MyMoneyGncReader() {} //**************************** Main Entry Point ************************************ #ifndef _GNCFILEANON void MyMoneyGncReader::readFile(QIODevice* pDevice, MyMoneyStorageMgr* storage) { Q_CHECK_PTR(pDevice); Q_CHECK_PTR(storage); m_storage = storage; qDebug("Entering gnucash importer"); setOptions(); // get a file anonymization factor from the user if (bAnonymize) setFileHideFactor(); //m_defaultPayee = createPayee (i18n("Unknown payee")); MyMoneyFile::instance()->attachStorage(m_storage); loadAllCurrencies(); MyMoneyFileTransaction ft; m_xr = new XmlReader(this); bool blocked = MyMoneyFile::instance()->signalsBlocked(); MyMoneyFile::instance()->blockSignals(true); try { m_xr->processFile(pDevice); terminate(); // do all the wind-up things ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Import failed:\n\n%1", QString::fromLatin1(e.what())), PACKAGE); qWarning("%s", e.what()); } // end catch MyMoneyFile::instance()->blockSignals(blocked); MyMoneyFile::instance()->detachStorage(m_storage); signalProgress(0, 1, i18n("Import complete")); // switch off progress bar delete m_xr; signalProgress(0, 1, i18nc("Application is ready to use", "Ready.")); // application is ready for input qDebug("Exiting gnucash importer"); } #else // Control code for the file anonymizer void MyMoneyGncReader::readFile(QString in, QString out) { QFile pDevice(in); if (!pDevice.open(QIODevice::ReadOnly)) qWarning("Can't open input file"); QFile outFile(out); if (!outFile.open(QIODevice::WriteOnly)) qWarning("Can't open output file"); oStream.setDevice(&outFile); bAnonymize = true; // get a file anonymization factor from the user setFileHideFactor(); m_xr = new XmlReader(this); try { m_xr->processFile(&pDevice); } catch (const MyMoneyException &e) { qWarning("%s", e->toLatin1()); } // end catch delete m_xr; pDevice.close(); outFile.close(); return ; } #include int main(int argc, char ** argv) { QApplication a(argc, argv); MyMoneyGncReader m; QString inFile, outFile; if (argc > 0) inFile = a.argv()[1]; if (argc > 1) outFile = a.argv()[2]; if (inFile.isEmpty()) { inFile = KFileDialog::getOpenFileName("", "Gnucash files(*.nc *)", 0); } if (inFile.isEmpty()) qWarning("Input file required"); if (outFile.isEmpty()) outFile = inFile + ".anon"; m.readFile(inFile, outFile); exit(0); } #endif // _GNCFILEANON void MyMoneyGncReader::setFileHideFactor() { #define MINFILEHIDEF 0.01 #define MAXFILEHIDEF 99.99 #if QT_VERSION >= QT_VERSION_CHECK(5,10,0) m_fileHideFactor = 0.0; while (m_fileHideFactor == 0.0) { m_fileHideFactor = QInputDialog::getDouble(0, i18n("Disguise your wealth"), i18n("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" "with a different value used for each transaction. In addition, to further disguise the true\n" "values, you may enter a number between %1 and %2 which will be applied to all values.\n" "These numbers will not be stored in the file.", MINFILEHIDEF, MAXFILEHIDEF), (1.0 + (int)(1000.0 * QRandomGenerator::system()->generate() / (RAND_MAX + 1.0))) / 100.0, MINFILEHIDEF, MAXFILEHIDEF, 2); } #else srand(QTime::currentTime().second()); // seed randomizer for anonymize m_fileHideFactor = 0.0; while (m_fileHideFactor == 0.0) { m_fileHideFactor = QInputDialog::getDouble(0, i18n("Disguise your wealth"), i18n("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" "with a different value used for each transaction. In addition, to further disguise the true\n" "values, you may enter a number between %1 and %2 which will be applied to all values.\n" "These numbers will not be stored in the file.", MINFILEHIDEF, MAXFILEHIDEF), - (1.0 + (int)(1000.0 * rand() / (RAND_MAX + 1.0))) / 100.0, + (1.0 + (int)(1000.0 * qrand() / (RAND_MAX + 1.0))) / 100.0, MINFILEHIDEF, MAXFILEHIDEF, 2); } #endif } #ifndef _GNCFILEANON //********************************* convertCommodity ******************************************* void MyMoneyGncReader::convertCommodity(const GncCommodity *gcm) { Q_CHECK_PTR(gcm); MyMoneySecurity equ; if (m_commodityCount == 0) signalProgress(0, m_gncCommodityCount, i18n("Loading commodities...")); if (!gcm->isCurrency()) { // currencies should not be present here but... equ.setName(gcm->name()); equ.setTradingSymbol(gcm->id()); equ.setTradingMarket(gcm->space()); // the 'space' may be market or quote source, dep on what the user did // don't set the source here since he may not want quotes //equ.setValue ("kmm-online-source", gcm->space()); // we don't know, so use it as both equ.setTradingCurrency(""); // not available here, will set from pricedb or transaction equ.setSecurityType(Security::Type::Stock); // default to it being a stock //tell the storage objects we have a new equity object. equ.setSmallestAccountFraction(gcm->fraction().toInt()); m_storage->addSecurity(equ); //assign the gnucash id as the key into the map to find our id if (gncdebug) qDebug() << "mapping, key =" << gcm->id() << "id =" << equ.id(); m_mapEquities[gcm->id().toUtf8()] = equ.id(); } signalProgress(++m_commodityCount, 0); return ; } //******************************* convertPrice ************************************************ void MyMoneyGncReader::convertPrice(const GncPrice *gpr) { Q_CHECK_PTR(gpr); // add this to our price history if (m_priceCount == 0) signalProgress(0, 1, i18n("Loading prices...")); MyMoneyMoney rate(convBadValue(gpr->value())); if (gpr->commodity()->isCurrency()) { MyMoneyPrice exchangeRate(gpr->commodity()->id().toUtf8(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!exchangeRate.rate(QString()).isZero()) m_storage->addPrice(exchangeRate); } else { MyMoneySecurity e = m_storage->security(m_mapEquities[gpr->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Searching map, key = " << gpr->commodity()->id() << ", found id =" << e.id().data(); e.setTradingCurrency(gpr->currency()->id().toUtf8()); MyMoneyPrice stockPrice(e.id(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!stockPrice.rate(QString()).isZero()) m_storage->addPrice(stockPrice); m_storage->modifySecurity(e); } signalProgress(++m_priceCount, 0); return ; } //*********************************convertAccount **************************************** void MyMoneyGncReader::convertAccount(const GncAccount* gac) { Q_CHECK_PTR(gac); TRY { // we don't care about the GNC root account if ("ROOT" == gac->type()) { m_rootId = gac->id().toUtf8(); return; } MyMoneyAccount acc; if (m_accountCount == 0) signalProgress(0, m_gncAccountCount, i18n("Loading accounts...")); acc.setName(gac->name()); acc.setDescription(gac->desc()); QDate currentDate = QDate::currentDate(); acc.setOpeningDate(currentDate); acc.setLastModified(currentDate); acc.setLastReconciliationDate(currentDate); if (gac->commodity()->isCurrency()) { acc.setCurrencyId(gac->commodity()->id().toUtf8()); m_currencyCount[gac->commodity()->id()]++; } acc.setParentAccountId(gac->parent().toUtf8()); // now determine the account type and its parent id /* This list taken from # Feb 2006: A RELAX NG Compact schema for gnucash "v2" XML files. # Copyright (C) 2006 Joshua Sled "NO_TYPE" "BANK" "CASH" "CREDIT" "ASSET" "LIABILITY" "STOCK" "MUTUAL" "CURRENCY" "INCOME" "EXPENSE" "EQUITY" "RECEIVABLE" "PAYABLE" "CHECKING" "SAVINGS" "MONEYMRKT" "CREDITLINE" Some don't seem to be used in practice. Not sure what CREDITLINE s/be converted as. */ if ("BANK" == gac->type() || "CHECKING" == gac->type()) { acc.setAccountType(Account::Type::Checkings); } else if ("SAVINGS" == gac->type()) { acc.setAccountType(Account::Type::Savings); } else if ("ASSET" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("CASH" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("CURRENCY" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("STOCK" == gac->type() || "MUTUAL" == gac->type()) { // gnucash allows a 'broker' account to be denominated as type STOCK, but with // a currency balance. We do not need to create a stock account for this // actually, the latest version of gnc (1.8.8) doesn't seem to allow you to do // this any more, though I do have one in my own account... if (gac->commodity()->isCurrency()) { acc.setAccountType(Account::Type::Investment); } else { acc.setAccountType(Account::Type::Stock); } } else if ("EQUITY" == gac->type()) { acc.setAccountType(Account::Type::Equity); } else if ("LIABILITY" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("CREDIT" == gac->type()) { acc.setAccountType(Account::Type::CreditCard); } else if ("INCOME" == gac->type()) { acc.setAccountType(Account::Type::Income); } else if ("EXPENSE" == gac->type()) { acc.setAccountType(Account::Type::Expense); } else if ("RECEIVABLE" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("PAYABLE" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("MONEYMRKT" == gac->type()) { acc.setAccountType(Account::Type::MoneyMarket); } else { // we have here an account type we can't currently handle throw MYMONEYEXCEPTION(QString::fromLatin1("Current importer does not recognize GnuCash account type %1").arg(gac->type())); } // if no parent account is present, assign to one of our standard accounts if ((acc.parentAccountId().isEmpty()) || (acc.parentAccountId() == m_rootId)) { switch (acc.accountGroup()) { case Account::Type::Asset: acc.setParentAccountId(m_storage->asset().id()); break; case Account::Type::Liability: acc.setParentAccountId(m_storage->liability().id()); break; case Account::Type::Income: acc.setParentAccountId(m_storage->income().id()); break; case Account::Type::Expense: acc.setParentAccountId(m_storage->expense().id()); break; case Account::Type::Equity: acc.setParentAccountId(m_storage->equity().id()); break; default: break; // not necessary but avoids compiler warnings } } // extra processing for a stock account if (acc.accountType() == Account::Type::Stock) { // save the id for later linking to investment account m_stockList.append(gac->id()); // set the equity type MyMoneySecurity e = m_storage->security(m_mapEquities[gac->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Acct equity search, key =" << gac->commodity()->id() << "found id =" << e.id(); acc.setCurrencyId(e.id()); // actually, the security id if ("MUTUAL" == gac->type()) { e.setSecurityType(Security::Type::MutualFund); if (gncdebug) qDebug() << "Setting" << e.name() << "to mutual"; m_storage->modifySecurity(e); } QString priceSource = gac->getKvpValue("price-source", "string"); if (!priceSource.isEmpty()) getPriceSource(e, priceSource); } if (gac->getKvpValue("tax-related", "integer") == QChar('1')) acc.setValue("Tax", "Yes"); // all the details from the file about the account should be known by now. // calling addAccount will automatically fill in the account ID. m_storage->addAccount(acc); m_mapIds[gac->id().toUtf8()] = acc.id(); // to link gnucash id to ours for tx posting if (gncdebug) qDebug() << "Gnucash account" << gac->id() << "has id of" << acc.id() << ", type of" << MyMoneyAccount::accountTypeToString(acc.accountType()) << "parent is" << acc.parentAccountId(); signalProgress(++m_accountCount, 0); return ; } PASS } //********************************************** convertTransaction ***************************** void MyMoneyGncReader::convertTransaction(const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_transactionCount == 0) signalProgress(0, m_gncTransactionCount, i18n("Loading transactions...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) m_txPayeeId = createPayee(gtx->desc()); tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); // save for use in splits m_txChequeNo = gtx->no(); // ditto tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save in storage, maybe needed for Orphan accounts // process splits for (i = 0; i < gtx->splitCount(); i++) { convertSplit(static_cast(gtx->getSplit(i))); } // handle the odd case of just one split, which gnc allows, // by just duplicating the split // of course, we should change the sign but this case has only ever been seen // when the balance is zero, and can cause kmm to crash, so... if (gtx->splitCount() == 1) { convertSplit(static_cast(gtx->getSplit(0))); } m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Link them to the tx. also, determine the // action type, and fill in some fields which gnc holds at transaction level // first off, is it a transfer (can only have 2 splits?) // also, a tx with just 2 splits is shown by GnuCash as non-split bool nonSplitTx = true; if (m_splitList.count() != 2) { m_potentialTransfer = false; nonSplitTx = false; } QString slotMemo = gtx->getKvpValue(QString("notes")); if (!slotMemo.isEmpty()) tx.setMemo(slotMemo); QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; // at this point, if m_potentialTransfer is still true, it is actually one! if (m_potentialTransfer) split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)); if ((m_useTxNotes) // if use txnotes option is set && (nonSplitTx) // and it's a (GnuCash) non-split transaction && (!tx.memo().isEmpty())) // and tx notes are present split.setMemo(tx.memo()); // use the tx notes as memo tx.addSplit(split); it = m_splitList.erase(it); } m_storage->addTransaction(tx, true); // all done, add the transaction to storage signalProgress(++m_transactionCount, 0); return ; } //******************************************convertSplit******************************** void MyMoneyGncReader::convertSplit(const GncSplit *gsp) { Q_CHECK_PTR(gsp); MyMoneySplit split; MyMoneyAccount splitAccount; // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gsp->acct().toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gsp->acct()); } // find the account pointer and save for later splitAccount = m_storage->account(kmmAccountId); // print some data so we can maybe identify this split later // TODO : prints personal data //if (gncdebug) qDebug ("Split data - gncid %s, kmmid %s, memo %s, value %s, recon state %s", // gsp->acct().toLatin1(), kmmAccountId.data(), gsp->memo().toLatin1(), gsp->value().toLatin1(), // gsp->recon().toLatin1()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // reconciled state and date switch (gsp->recon().at(0).toLatin1()) { case 'n': split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; case 'c': split.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case 'y': split.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; } split.setReconcileDate(gsp->reconDate()); // memo split.setMemo(gsp->memo()); // accountId split.setAccountId(kmmAccountId); // cheque no split.setNumber(m_txChequeNo); // value and quantity MyMoneyMoney splitValue(convBadValue(gsp->value())); if (gsp->value() == "-1/0") { // treat gnc invalid value as zero // it's not quite a consistency check, but easier to treat it as such m_messageList["CC"].append (i18n("Account or Category %1, transaction date %2; split contains invalid value; please check", splitAccount.name(), m_txDatePosted.toString(Qt::ISODate))); } MyMoneyMoney splitQuantity(convBadValue(gsp->qty())); split.setValue(splitValue); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(splitValue); } else { split.setShares(splitQuantity); } // in kmm, the first split is important. in this routine we will // save the splits in our split list with the priority: // 1. assets // 2. liabilities // 3. others (categories) // but keeping each in same order as gnucash switch (splitAccount.accountGroup()) { case Account::Type::Asset: if (splitAccount.accountType() == Account::Type::Stock) { split.value().isZero() ? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) : // free shares? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); m_potentialTransfer = false; // ? // add a price history entry MyMoneySecurity e = m_storage->security(splitAccount.currencyId()); MyMoneyMoney price; if (!split.shares().isZero()) { static const signed64 NEW_DENOM = 10000; price = split.value() / split.shares(); price = MyMoneyMoney(price.toDouble(), NEW_DENOM); } if (!price.isZero()) { TRY { // we can't use m_storage->security coz security list is not built yet m_storage->currency(m_txCommodity); // will throw exception if not currency e.setTradingCurrency(m_txCommodity); if (gncdebug) qDebug() << "added price for" << e.name() << price.toString() << "date" << m_txDatePosted.toString(Qt::ISODate); m_storage->modifySecurity(e); MyMoneyPrice dealPrice(e.id(), m_txCommodity, m_txDatePosted, price, i18n("Imported Transaction")); m_storage->addPrice(dealPrice); } CATCH { // stock transfer; treat like free shares? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)); } } } else { // not stock if (split.value().isNegative()) { bool isNumeric = false; if (!split.number().isEmpty()) { split.number().toLong(&isNumeric); // No QString.isNumeric()?? } if (isNumeric) { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Check)); } else { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); } } else { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); } } m_splitList.append(split); break; case Account::Type::Liability: split.value().isNegative() ? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)) : split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); m_liabilitySplitList.append(split); break; default: m_potentialTransfer = false; m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertTemplateTransaction ********************************************** MyMoneyTransaction MyMoneyGncReader::convertTemplateTransaction(const QString& schedName, const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_templateCount == 0) signalProgress(0, 1, i18n("Loading templates...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) { m_txPayeeId = createPayee(gtx->desc()); } else { m_txPayeeId = createPayee(i18n("Unknown payee")); // schedules require a payee tho normal tx's don't. not sure why... } tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save for possible use in orphan account // process splits for (i = 0; i < gtx->splitCount(); i++) { convertTemplateSplit(schedName, static_cast(gtx->getSplit(i))); } // determine the action type for the splits and link them to the template tx if (!m_otherSplitList.isEmpty()) m_potentialTransfer = false; // tfrs can occur only between assets and asset/liabilities m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Transfer them to the tx // also, determine the action type. first off, is it a transfer (can only have 2 splits?) if (m_splitList.count() != 2) m_potentialTransfer = false; // at this point, if m_potentialTransfer is still true, it is actually one! QString txMemo = ""; QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; if (m_potentialTransfer) { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)); } else { if (split.value().isNegative()) { //split.setAction (negativeActionType); split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); } else { //split.setAction (positiveActionType); split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); } } split.setNumber(gtx->no()); // set cheque no (or equivalent description) // Arbitrarily, save the first non-null split memo as the memo for the whole tx // I think this is necessary because txs with just 2 splits (the majority) // are not viewable as split transactions in kmm so the split memo is not seen if ((txMemo.isEmpty()) && (!split.memo().isEmpty())) txMemo = split.memo(); tx.addSplit(split); it = m_splitList.erase(it); } // memo - set from split tx.setMemo(txMemo); signalProgress(++m_templateCount, 0); return (tx); } //********************************* convertTemplateSplit **************************************************** void MyMoneyGncReader::convertTemplateSplit(const QString& schedName, const GncTemplateSplit *gsp) { Q_CHECK_PTR(gsp); // convertTemplateSplit MyMoneySplit split; MyMoneyAccount splitAccount; unsigned int i, j; bool nonNumericFormula = false; // action, value and account will be set from slots // reconcile state, always Not since it hasn't even been posted yet (?) split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // memo split.setMemo(gsp->memo()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // read split slots (KVPs) int xactionCount = 0; int validSlotCount = 0; QString gncAccountId; for (i = 0; i < gsp->kvpCount(); i++) { const GncKvp& slot = gsp->getKvp(i); if ((slot.key() == "sched-xaction") && (slot.type() == "frame")) { bool bFoundStringCreditFormula = false; bool bFoundStringDebitFormula = false; bool bFoundGuidAccountId = false; QString gncCreditFormula, gncDebitFormula; for (j = 0; j < slot.kvpCount(); j++) { const GncKvp& subSlot = slot.getKvp(j); // again, see comments above. when we have a full specification // of all the options available to us, we can no doubt improve on this if ((subSlot.key() == "credit-formula") && (subSlot.type() == "string")) { gncCreditFormula = subSlot.value(); bFoundStringCreditFormula = true; } if ((subSlot.key() == "debit-formula") && (subSlot.type() == "string")) { gncDebitFormula = subSlot.value(); bFoundStringDebitFormula = true; } if ((subSlot.key() == "account") && (subSlot.type() == "guid")) { gncAccountId = subSlot.value(); bFoundGuidAccountId = true; } } // all data read, now check we have everything if ((bFoundStringCreditFormula) && (bFoundStringDebitFormula) && (bFoundGuidAccountId)) { if (gncdebug) qDebug() << "Found valid slot; credit" << gncCreditFormula << "debit" << gncDebitFormula << "acct" << gncAccountId; validSlotCount++; } // validate numeric, work out sign MyMoneyMoney exFormula; exFormula.setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QString numericTest; char crdr = 0 ; if (!gncCreditFormula.isEmpty()) { crdr = 'C'; numericTest = gncCreditFormula; } else if (!gncDebitFormula.isEmpty()) { crdr = 'D'; numericTest = gncDebitFormula; } KMyMoneyMoneyValidator v(0); int pos; // useless, but required for validator if (v.validate(numericTest, pos) == QValidator::Acceptable) { switch (crdr) { case 'C': exFormula = QString("-" + numericTest); break; case 'D': exFormula = numericTest; } } else { if (gncdebug) qDebug() << numericTest << "is not numeric"; nonNumericFormula = true; } split.setValue(exFormula); xactionCount++; } else { m_messageList["SC"].append( i18n("Schedule %1 contains unknown action (key = %2, type = %3)", schedName, slot.key(), slot.type())); m_suspectSchedule = true; } } // report this as untranslatable tx if (xactionCount > 1) { m_messageList["SC"].append( i18n("Schedule %1 contains multiple actions; only one has been imported", schedName)); m_suspectSchedule = true; } if (validSlotCount == 0) { m_messageList["SC"].append( i18n("Schedule %1 contains no valid splits", schedName)); m_suspectSchedule = true; } if (nonNumericFormula) { m_messageList["SC"].append( i18n("Schedule %1 appears to contain a formula. GnuCash formulae are not convertible", schedName)); m_suspectSchedule = true; } // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gncAccountId.toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gncAccountId); } splitAccount = m_storage->account(kmmAccountId); split.setAccountId(kmmAccountId); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(split.value()); } /* else { //FIXME: scheduled currency or investment tx needs to be investigated split.setShares (splitQuantity); } */ // add the split to one of the lists switch (splitAccount.accountGroup()) { case Account::Type::Asset: m_splitList.append(split); break; case Account::Type::Liability: m_liabilitySplitList.append(split); break; default: m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertSchedule ******************************************************** void MyMoneyGncReader::convertSchedule(const GncSchedule *gsc) { TRY { Q_CHECK_PTR(gsc); MyMoneySchedule sc; MyMoneyTransaction tx; m_suspectSchedule = false; QDate nextDate, lastDate, endDate; // for date calculations QDate today = QDate::currentDate(); int numOccurs, remOccurs; if (m_scheduleCount == 0) signalProgress(0, m_gncScheduleCount, i18n("Loading schedules...")); // schedule name sc.setName(gsc->name()); // find the transaction template as stored earlier QList::const_iterator itt; for (itt = m_templateList.constBegin(); itt != m_templateList.constEnd(); ++itt) { // the id to match against is the split:account value in the splits if (static_cast((*itt)->getSplit(0))->acct() == gsc->templId()) break; } if (itt == m_templateList.constEnd()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot find template transaction for schedule %1").arg(sc.name())); } else { tx = convertTemplateTransaction(sc.name(), *itt); } tx.clearId(); // define the conversion table for intervals struct convIntvl { QString gncType; // the gnucash name unsigned char interval; // for date calculation unsigned int intervalCount; Schedule::Occurrence occ; // equivalent occurrence code Schedule::WeekendOption wo; }; /* other intervals supported by gnc according to Josh Sled's schema (see above) "none" "semi_monthly" */ /* some of these type names do not appear in gnucash and are difficult to generate for pre 2.2 files.They can be generated for 2.2 however, by GncRecurrence::getFrequency() */ static convIntvl vi [] = { {"once", 'o', 1, Schedule::Occurrence::Once, Schedule::WeekendOption::MoveNothing }, {"daily" , 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveNothing }, //{"daily_mf", 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveAfter }, doesn't work, need new freq in kmm {"30-days" , 'd', 30, Schedule::Occurrence::EveryThirtyDays, Schedule::WeekendOption::MoveNothing }, {"weekly", 'w', 1, Schedule::Occurrence::Weekly, Schedule::WeekendOption::MoveNothing }, {"bi_weekly", 'w', 2, Schedule::Occurrence::EveryOtherWeek, Schedule::WeekendOption::MoveNothing }, {"three-weekly", 'w', 3, Schedule::Occurrence::EveryThreeWeeks, Schedule::WeekendOption::MoveNothing }, {"four-weekly", 'w', 4, Schedule::Occurrence::EveryFourWeeks, Schedule::WeekendOption::MoveNothing }, {"eight-weekly", 'w', 8, Schedule::Occurrence::EveryEightWeeks, Schedule::WeekendOption::MoveNothing }, {"monthly", 'm', 1, Schedule::Occurrence::Monthly, Schedule::WeekendOption::MoveNothing }, {"two-monthly", 'm', 2, Schedule::Occurrence::EveryOtherMonth, Schedule::WeekendOption::MoveNothing }, {"quarterly", 'm', 3, Schedule::Occurrence::Quarterly, Schedule::WeekendOption::MoveNothing }, {"tri_annually", 'm', 4, Schedule::Occurrence::EveryFourMonths, Schedule::WeekendOption::MoveNothing }, {"semi_yearly", 'm', 6, Schedule::Occurrence::TwiceYearly, Schedule::WeekendOption::MoveNothing }, {"yearly", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing }, {"two-yearly", 'y', 2, Schedule::Occurrence::EveryOtherYear, Schedule::WeekendOption::MoveNothing }, {"zzz", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing} // zzz = stopper, may cause problems. what else can we do? }; QString frequency = "unknown"; // set default to unknown frequency bool unknownOccurs = false; // may have zero, or more than one frequency/recurrence spec QString schedEnabled; if (gsc->version() == "2.0.0") { if (gsc->m_vpRecurrence.count() != 1) { unknownOccurs = true; } else { const GncRecurrence *gre = gsc->m_vpRecurrence.first(); //qDebug (QString("Sched %1, pt %2, mu %3, sd %4").arg(gsc->name()).arg(gre->periodType()) // .arg(gre->mult()).arg(gre->startDate().toString(Qt::ISODate))); frequency = gre->getFrequency(); schedEnabled = gsc->enabled(); } sc.setOccurrence(Schedule::Occurrence::Once); // FIXME - how to convert } else { // find this interval const GncFreqSpec *fs = gsc->getFreqSpec(); if (fs == 0) { unknownOccurs = true; } else { frequency = fs->intervalType(); if (!fs->m_fsList.isEmpty()) unknownOccurs = true; // nested freqspec } schedEnabled = 'y'; // earlier versions did not have an enable flag } int i; for (i = 0; vi[i].gncType != "zzz"; i++) { if (frequency == vi[i].gncType) break; } if (vi[i].gncType == "zzz") { m_messageList["SC"].append( i18n("Schedule %1 has interval of %2 which is not currently available", sc.name(), frequency)); i = 0; // treat as single occurrence m_suspectSchedule = true; } if (unknownOccurs) { m_messageList["SC"].append( i18n("Schedule %1 contains unknown interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // set the occurrence interval, weekend option, start date sc.setOccurrence(vi[i].occ); sc.setWeekendOption(vi[i].wo); sc.setStartDate(gsc->startDate()); // if a last date was specified, use it, otherwise try to work out the last date sc.setLastPayment(gsc->lastDate()); numOccurs = gsc->numOccurs().toInt(); if (sc.lastPayment() == QDate()) { nextDate = lastDate = gsc->startDate(); while ((nextDate < today) && (numOccurs-- != 0)) { lastDate = nextDate; nextDate = incrDate(lastDate, vi[i].interval, vi[i].intervalCount); } sc.setLastPayment(lastDate); } // under Tom's new regime, the tx dates are the next due date (I think) tx.setPostDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); tx.setEntryDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); // if an end date was specified, use it, otherwise if the input file had a number // of occurs remaining, work out the end date sc.setEndDate(gsc->endDate()); numOccurs = gsc->numOccurs().toInt(); remOccurs = gsc->remOccurs().toInt(); if ((sc.endDate() == QDate()) && (remOccurs > 0)) { endDate = sc.lastPayment(); while (remOccurs-- > 0) { endDate = incrDate(endDate, vi[i].interval, vi[i].intervalCount); } sc.setEndDate(endDate); } // Check for sched deferred interval. Don't know how/if we can handle it, or even what it means... if (gsc->getSchedDef() != 0) { m_messageList["SC"].append( i18n("Schedule %1 contains a deferred interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // payment type, options sc.setPaymentType((Schedule::PaymentType)Schedule::PaymentType::Other); sc.setFixed(!m_suspectSchedule); // if any probs were found, set it as variable so user will always be prompted // we don't currently have a 'disable' option, but just make sure auto-enter is off if not enabled //qDebug(QString("%1 and %2").arg(gsc->autoCreate()).arg(schedEnabled)); sc.setAutoEnter((gsc->autoCreate() == QChar('y')) && (schedEnabled == QChar('y'))); //qDebug(QString("autoEnter set to %1").arg(sc.autoEnter())); // type QString actionType = tx.splits().first().action(); if (actionType == MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)) { sc.setType((Schedule::Type)Schedule::Type::Deposit); } else if (actionType == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { sc.setType((Schedule::Type)Schedule::Type::Transfer); } else { sc.setType((Schedule::Type)Schedule::Type::Bill); } // finally, set the transaction pointer sc.setTransaction(tx); //tell the storage objects we have a new schedule object. if (m_suspectSchedule && m_dropSuspectSchedules) { m_messageList["SC"].append( i18n("Schedule %1 dropped at user request", sc.name())); } else { m_storage->addSchedule(sc); if (m_suspectSchedule) m_suspectList.append(sc.id()); } signalProgress(++m_scheduleCount, 0); return ; } PASS } //********************************* convertFreqSpec ******************************************************** void MyMoneyGncReader::convertFreqSpec(const GncFreqSpec *) { // Nowt to do here at the moment, convertSched only retrieves the interval type // but we will probably need to look into the nested freqspec when we properly implement semi-monthly and stuff return ; } //********************************* convertRecurrence ******************************************************** void MyMoneyGncReader::convertRecurrence(const GncRecurrence *) { return ; } //********************************************************************************************************** //************************************* terminate ********************************************************** void MyMoneyGncReader::terminate() { TRY { // All data has been converted and added to storage // this code is just temporary to show us what is in the file. if (gncdebug) qDebug("%d accounts found in the GnuCash file", (unsigned int)m_mapIds.count()); for (map_accountIds::const_iterator it = m_mapIds.constBegin(); it != m_mapIds.constEnd(); ++it) { if (gncdebug) qDebug() << "key =" << it.key() << "value =" << it.value(); } // first step is to implement the users investment option, now we // have all the accounts available QList::iterator stocks; for (stocks = m_stockList.begin(); stocks != m_stockList.end(); ++stocks) { checkInvestmentOption(*stocks); } // Next step is to walk the list and assign the parent/child relationship between the objects. unsigned int i = 0; signalProgress(0, m_accountCount, i18n("Reorganizing accounts...")); QList list; QList::iterator acc; m_storage->accountList(list); for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).parentAccountId() == m_storage->asset().id()) { MyMoneyAccount assets = m_storage->asset(); m_storage->addAccount(assets, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main asset account"; } else if ((*acc).parentAccountId() == m_storage->liability().id()) { MyMoneyAccount liabilities = m_storage->liability(); m_storage->addAccount(liabilities, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main liability account"; } else if ((*acc).parentAccountId() == m_storage->income().id()) { MyMoneyAccount incomes = m_storage->income(); m_storage->addAccount(incomes, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main income account"; } else if ((*acc).parentAccountId() == m_storage->expense().id()) { MyMoneyAccount expenses = m_storage->expense(); m_storage->addAccount(expenses, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main expense account"; } else if ((*acc).parentAccountId() == m_storage->equity().id()) { MyMoneyAccount equity = m_storage->equity(); m_storage->addAccount(equity, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main equity account"; } else if ((*acc).parentAccountId() == m_rootId) { if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main root account"; } else { // it is not under one of the main accounts, so find gnucash parent QString parentKey = (*acc).parentAccountId(); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of " << (*acc).parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { if (gncdebug) qDebug() << "Setting account id" << (*acc).id() << "parent account id to" << id.value(); MyMoneyAccount parent = m_storage->account(id.value()); parent = checkConsistency(parent, (*acc)); m_storage->addAccount(parent, (*acc)); } else { throw MYMONEYEXCEPTION_CSTRING("terminate() could not find account id"); } } signalProgress(++i, 0); } // end for account signalProgress(0, 1, (".")); // debug - get rid of reorg message // offer the most common account currency as a default QString mainCurrency = ""; unsigned int maxCount = 0; QMap::ConstIterator it; for (it = m_currencyCount.constBegin(); it != m_currencyCount.constEnd(); ++it) { if (it.value() > maxCount) { maxCount = it.value(); mainCurrency = it.key(); } } if (mainCurrency != "") { QString question = i18n("Your main currency seems to be %1 (%2); do you want to set this as your base currency?", mainCurrency, m_storage->currency(mainCurrency.toUtf8()).name()); if (KMessageBox::questionYesNo(0, question, PACKAGE) == KMessageBox::Yes) { m_storage->setValue("kmm-baseCurrency", mainCurrency); } } // now produce the end of job reports - first, work out which ones are required QList sectionsToReport; // list of sections needing report sectionsToReport.append("MN"); // always build the main section if ((m_ccCount = m_messageList["CC"].count()) > 0) sectionsToReport.append("CC"); if ((m_orCount = m_messageList["OR"].count()) > 0) sectionsToReport.append("OR"); if ((m_scCount = m_messageList["SC"].count()) > 0) sectionsToReport.append("SC"); // produce the sections in separate message boxes bool exit = false; int si; for (si = 0; (si < sectionsToReport.count()) && !exit; ++si) { QString button0Text = i18nc("Button to show more detailed data", "More"); if (si + 1 == sectionsToReport.count()) button0Text = i18nc("Button to close the current dialog", "Done"); // last section KGuiItem yesItem(button0Text, QIcon(), "", ""); KGuiItem noItem(i18n("Save Report"), QIcon(), "", ""); switch (KMessageBox::questionYesNoCancel(0, buildReportSection(sectionsToReport[si]), PACKAGE, yesItem, noItem)) { case KMessageBox::Yes: break; case KMessageBox::No: exit = writeReportToFile(sectionsToReport); break; default: exit = true; break; } } for (si = 0; si < m_suspectList.count(); ++si) { auto sc = m_storage->schedule(m_suspectList[si]); KMessageBox::information(0, i18n("Problems were encountered in converting schedule '%1'.", sc.name()), PACKAGE); // TODO: return this feature // switch (KMessageBox::warningYesNo(0, i18n("Problems were encountered in converting schedule '%1'.\nDo you want to review or edit it now?", sc.name()), PACKAGE)) { // case KMessageBox::Yes: // auto s = new KEditScheduleDlg(sc); // if (s->exec()) // m_storage->modifySchedule(s->schedule()); // delete s; // break; // default: // break; // } } } PASS } //************************************ buildReportSection************************************ QString MyMoneyGncReader::buildReportSection(const QString& source) { TRY { QString s = ""; bool more = false; if (source == "MN") { s.append(i18n("Found:\n\n")); s.append(i18np("%1 commodity (equity)\n", "%1 commodities (equities)\n", m_commodityCount)); s.append(i18np("%1 price\n", "%1 prices\n", m_priceCount)); s.append(i18np("%1 account\n", "%1 accounts\n", m_accountCount)); s.append(i18np("%1 transaction\n", "%1 transactions\n", m_transactionCount)); s.append(i18np("%1 schedule\n", "%1 schedules\n", m_scheduleCount)); s.append("\n\n"); if (m_ccCount == 0) { s.append(i18n("No inconsistencies were detected\n")); } else { s.append(i18np("%1 inconsistency was detected and corrected\n", "%1 inconsistencies were detected and corrected\n", m_ccCount)); more = true; } if (m_orCount > 0) { s.append("\n\n"); s.append(i18np("%1 orphan account was created\n", "%1 orphan accounts were created\n", m_orCount)); more = true; } if (m_scCount > 0) { s.append("\n\n"); s.append(i18np("%1 possible schedule problem was noted\n", "%1 possible schedule problems were noted\n", m_scCount)); more = true; } QString unsupported(""); QString lineSep("\n - "); if (m_smallBusinessFound) unsupported.append(lineSep + i18n("Small Business Features (Customers, Invoices, etc.)")); if (m_budgetsFound) unsupported.append(lineSep + i18n("Budgets")); if (m_lotsFound) unsupported.append(lineSep + i18n("Lots")); if (!unsupported.isEmpty()) { unsupported.prepend(i18n("The following features found in your file are not currently supported:")); s.append(unsupported); } if (more) s.append(i18n("\n\nPress More for further information")); } else { s = m_messageList[source].join(QChar('\n')); } if (gncdebug) qDebug() << s; return (static_cast(s)); } PASS } //************************ writeReportToFile********************************* bool MyMoneyGncReader::writeReportToFile(const QList& sectionsToReport) { TRY { int i; QString fd = QFileDialog::getSaveFileName(0, QString(), QString(), i18n("Save report as")); if (fd.isEmpty()) return (false); QFile reportFile(fd); if (!reportFile.open(QIODevice::WriteOnly)) { return (false); } QTextStream stream(&reportFile); for (i = 0; i < sectionsToReport.count(); i++) stream << buildReportSection(sectionsToReport[i]) << endl; reportFile.close(); return (true); } PASS } /**************************************************************************** Utility routines *****************************************************************************/ //************************ createPayee *************************** QString MyMoneyGncReader::createPayee(const QString& gncDescription) { MyMoneyPayee payee; TRY { payee = m_storage->payeeByName(gncDescription); } CATCH { // payee not found, create one payee.setName(gncDescription); m_storage->addPayee(payee); } return (payee.id()); } //************************************** createOrphanAccount ******************************* QString MyMoneyGncReader::createOrphanAccount(const QString& gncName) { MyMoneyAccount acc; acc.setName("orphan_" + gncName); acc.setDescription(i18n("Orphan created from unknown GnuCash account")); QDate today = QDate::currentDate(); acc.setOpeningDate(today); acc.setLastModified(today); acc.setLastReconciliationDate(today); acc.setCurrencyId(m_txCommodity); acc.setAccountType(Account::Type::Asset); acc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(acc); // assign the gnucash id as the key into the map to find our id m_mapIds[gncName.toUtf8()] = acc.id(); m_messageList["OR"].append( i18n("One or more transactions contain a reference to an otherwise unknown account\n" "An asset account with the name %1 has been created to hold the data", acc.name())); return (acc.id()); } //****************************** incrDate ********************************************* QDate MyMoneyGncReader::incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount) { TRY { switch (interval) { case 'd': return (lastDate.addDays(intervalCount)); case 'w': return (lastDate.addDays(intervalCount * 7)); case 'm': return (lastDate.addMonths(intervalCount)); case 'y': return (lastDate.addYears(intervalCount)); case 'o': // once-only return (lastDate); } throw MYMONEYEXCEPTION_CSTRING("Internal error - invalid interval char in incrDate"); // QDate r = QDate(); return (r); // to keep compiler happy } PASS } //********************************* checkConsistency ********************************** MyMoneyAccount MyMoneyGncReader::checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child) { TRY { // gnucash is flexible/weird enough to allow various inconsistencies // these are a couple I found in my file, no doubt more will be discovered if ((child.accountType() == Account::Type::Investment) && (parent.accountType() != Account::Type::Asset)) { m_messageList["CC"].append( i18n("An Investment account must be a child of an Asset account\n" "Account %1 will be stored under the main Asset account", child.name())); return m_storage->asset(); } if ((child.accountType() == Account::Type::Income) && (parent.accountType() != Account::Type::Income)) { m_messageList["CC"].append( i18n("An Income account must be a child of an Income account\n" "Account %1 will be stored under the main Income account", child.name())); return m_storage->income(); } if ((child.accountType() == Account::Type::Expense) && (parent.accountType() != Account::Type::Expense)) { m_messageList["CC"].append( i18n("An Expense account must be a child of an Expense account\n" "Account %1 will be stored under the main Expense account", child.name())); return m_storage->expense(); } return (parent); } PASS } //*********************************** checkInvestmentOption ************************* void MyMoneyGncReader::checkInvestmentOption(QString stockId) { // implement the investment option for stock accounts // first check whether the parent account (gnucash id) is actually an // investment account. if it is, no further action is needed MyMoneyAccount stockAcc = m_storage->account(m_mapIds[stockId.toUtf8()]); MyMoneyAccount parent; QString parentKey = stockAcc.parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { parent = m_storage->account(id.value()); if (parent.accountType() == Account::Type::Investment) return ; } // so now, check the investment option requested by the user // option 0 creates a separate investment account for each stock account if (m_investmentOption == 0) { MyMoneyAccount invAcc(stockAcc); invAcc.setAccountType(Account::Type::Investment); invAcc.setCurrencyId(QString("")); // we don't know what currency it is!! invAcc.setParentAccountId(parentKey); // intersperse it between old parent and child stock acct m_storage->addAccount(invAcc); m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << invAcc.name() << "as id" << invAcc.id() << "parent" << invAcc.parentAccountId(); if (gncdebug) qDebug() << "Setting stock" << stockAcc.name() << "id" << stockAcc.id() << "as child of" << invAcc.id(); stockAcc.setParentAccountId(invAcc.id()); m_storage->addAccount(invAcc, stockAcc); // investment option 1 creates a single investment account for all stocks } else if (m_investmentOption == 1) { static QString singleInvAccId = ""; MyMoneyAccount singleInvAcc; bool ok = false; if (singleInvAccId.isEmpty()) { // if the account has not yet been created QString invAccName; while (!ok) { invAccName = QInputDialog::getText(0, QStringLiteral(PACKAGE), i18n("Enter the investment account name "), QLineEdit::Normal, i18n("My Investments"), &ok); } singleInvAcc.setName(invAccName); singleInvAcc.setAccountType(Account::Type::Investment); singleInvAcc.setCurrencyId(QString("")); singleInvAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(singleInvAcc); m_mapIds [singleInvAcc.id()] = singleInvAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << singleInvAcc.name() << "as id" << singleInvAcc.id() << "parent" << singleInvAcc.parentAccountId() << "reparenting stock"; singleInvAccId = singleInvAcc.id(); } else { // the account has already been created singleInvAcc = m_storage->account(singleInvAccId); } m_storage->addAccount(singleInvAcc, stockAcc); // add stock as child // the original intention of option 2 was to allow any asset account to be converted to an investment (broker) account // however, since we have already stored the accounts as asset, we have no way at present of changing their type // the only alternative would be to hold all the gnucash data in memory, then implement this option, then convert all the data // that would mean a major overhaul of the code. Perhaps I'll think of another way... } else if (m_investmentOption == 2) { static int lastSelected = 0; MyMoneyAccount invAcc(stockAcc); QStringList accList; QList list; QList::iterator acc; m_storage->accountList(list); // build a list of candidates for the input box for (acc = list.begin(); acc != list.end(); ++acc) { // if (((*acc).accountGroup() == Account::Type::Asset) && ((*acc).accountType() != Account::Type::Stock)) accList.append ((*acc).name()); if ((*acc).accountType() == Account::Type::Investment) accList.append((*acc).name()); } //if (accList.isEmpty()) qWarning ("No available accounts"); bool ok = false; while (!ok) { // keep going till we have a valid investment parent QString invAccName = QInputDialog::getItem(0, PACKAGE, i18n("Select parent investment account or enter new name. Stock %1", stockAcc.name()), accList, lastSelected, true, &ok); if (ok) { lastSelected = accList.indexOf(invAccName); // preserve selection for next time for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).name() == invAccName) break; } if (acc != list.end()) { // an account was selected invAcc = *acc; } else { // a new account name was entered invAcc.setAccountType(Account::Type::Investment); invAcc.setName(invAccName); invAcc.setCurrencyId(QString("")); invAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(invAcc); ok = true; } if (invAcc.accountType() == Account::Type::Investment) { ok = true; } else { // this code is probably not going to be implemented coz we can't change account types (??) #if 0 QMessageBox mb(PACKAGE, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), QMessageBox::Question, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape, Qt::NoButton); switch (mb.exec()) { case QMessageBox::No : ok = false; break; default: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; } #endif switch (KMessageBox::questionYesNo(0, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), PACKAGE)) { case KMessageBox::Yes: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; default: ok = false; break; } } } // end if ok - user pressed Cancel } // end while !ok m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later m_storage->addAccount(invAcc, stockAcc); } else { // investment option != 0, 1, 2 qWarning("Invalid investment option %d", m_investmentOption); } } // get the price source for a stock (gnc account) where online quotes are requested void MyMoneyGncReader::getPriceSource(MyMoneySecurity stock, QString gncSource) { // if he wants to use Finance::Quote, no conversion of source name is needed if (m_useFinanceQuote) { stock.setValue("kmm-online-quote-system", "Finance::Quote"); stock.setValue("kmm-online-source", gncSource.toLower()); m_storage->modifySecurity(stock); return; } // first check if we have already asked about this source // (mapSources is initially empty. We may be able to pre-fill it with some equivalent // sources, if such things do exist. User feedback may help here.) QMap::const_iterator it; for (it = m_mapSources.constBegin(); it != m_mapSources.constEnd(); ++it) { if (it.key() == gncSource) { stock.setValue("kmm-online-source", it.value()); m_storage->modifySecurity(stock); return; } } // not found in map, so ask the user QPointer dlg = new KGncPriceSourceDlg(stock.name(), gncSource); dlg->exec(); QString s = dlg->selectedSource(); if (!s.isEmpty()) { stock.setValue("kmm-online-source", s); m_storage->modifySecurity(stock); } if (dlg->alwaysUse()) m_mapSources[gncSource] = s; delete dlg; return; } void MyMoneyGncReader::loadAllCurrencies() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; if (!file->currencyList().isEmpty()) return; auto ancientCurrencies = file->ancientCurrencies(); try { foreach (auto currency, file->availableCurrencyList()) { file->addCurrency(currency); MyMoneyPrice price = ancientCurrencies.value(currency, MyMoneyPrice()); if (price != MyMoneyPrice()) file->addPrice(price); } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s loading currency", e.what()); } } // functions to control the progress bar //*********************** setProgressCallback ***************************** void MyMoneyGncReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; return ; } //************************** signalProgress ******************************* void MyMoneyGncReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); return ; } #endif // _GNCFILEANON diff --git a/kmymoney/plugins/views/reports/reportsview.cpp b/kmymoney/plugins/views/reports/reportsview.cpp index 5953fe67a..eb4974655 100644 --- a/kmymoney/plugins/views/reports/reportsview.cpp +++ b/kmymoney/plugins/views/reports/reportsview.cpp @@ -1,330 +1,330 @@ /*************************************************************************** reportsview.cpp ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * 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 "reportsview.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "viewinterface.h" #include "kreportsview.h" #include "kreportchartview.h" #include "kmymoneysettings.h" #include "pivottable.h" #include "pivotgrid.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyenums.h" #include "reportsviewenums.h" #define VIEW_LEDGER "ledger" ReportsView::ReportsView(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "reportsview"/*must be the same as X-KDE-PluginInfo-Name*/), m_view(nullptr) { Q_UNUSED(args) setComponentName("reportsview", i18n("Reports view")); // For information, announce that we have been loaded. qDebug("Plugins: reportsview loaded"); } ReportsView::~ReportsView() { qDebug("Plugins: reportsview unloaded"); } void ReportsView::plug() { m_view = new KReportsView; viewInterface()->addView(m_view, i18n("Reports"), View::Reports); } void ReportsView::unplug() { viewInterface()->removeView(View::Reports); } QVariant ReportsView::requestData(const QString &arg, uint type) { switch(type) { case eWidgetPlugin::WidgetType::NetWorthForecast: return QVariant::fromValue(netWorthForecast()); case eWidgetPlugin::WidgetType::NetWorthForecastWithArgs: return QVariant::fromValue(netWorthForecast(arg)); case eWidgetPlugin::WidgetType::Budget: return QVariant(budget()); default: return QVariant(); } } QWidget *ReportsView::netWorthForecast() const { MyMoneyReport reportCfg = MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartDataLabels(false); reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingSchedules(false); reportCfg.addAccountGroup(eMyMoney::Account::Type::Asset); reportCfg.addAccountGroup(eMyMoney::Account::Type::Liability); reportCfg.setColumnsAreDays(true); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(+ 90)); reports::PivotTable table(reportCfg); auto chartWidget = new reports::KReportChartView(nullptr); table.drawChart(*chartWidget); return chartWidget; } QWidget *ReportsView::netWorthForecast(const QString &arg) const { const QStringList liArgs = arg.split(';'); if (liArgs.count() != 4) return new QWidget(); eMyMoney::Report::DetailLevel detailLevel[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; MyMoneyReport reportCfg = MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below detailLevel[liArgs.at(0).toInt()], i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingSchedules(false); reportCfg.setColumnsAreDays( true ); reportCfg.setChartDataLabels(false); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(liArgs.at(2).toLongLong())); reports::PivotTable table(reportCfg); auto forecastChart = new reports::KReportChartView(nullptr); forecastChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); table.drawChart(*forecastChart); // Adjust the size forecastChart->resize(liArgs.at(2).toInt() - 10, liArgs.at(3).toInt()); forecastChart->show(); forecastChart->update(); return forecastChart; } QString ReportsView::budget() const { const auto file = MyMoneyFile::instance(); QString html; if (file->countBudgets() == 0) { html += QString(""); html += QString("
%1
").arg(i18n("You have no budgets to display.")); html += QString(""); return html; } auto prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); bool isOverrun = false; int i = 0; //config report just like "Monthly Budgeted vs Actual MyMoneyReport reportCfg = MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Monthly Budgeted vs. Actual"), i18n("Generated Report")); reportCfg.setBudget("Any", true); reports::PivotTable table(reportCfg); reports::PivotGrid grid = table.grid(); //div header html += "
" + i18n("Budget") + "
\n
 
\n"; //display budget summary html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += QString(""); MyMoneyMoney totalBudgetValue = grid.m_total[reports::eBudget].m_total; MyMoneyMoney totalActualValue = grid.m_total[reports::eActual].m_total; MyMoneyMoney totalBudgetDiffValue = grid.m_total[reports::eBudgetDiff].m_total; QString totalBudgetAmount = totalBudgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString totalActualAmount = totalActualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString totalBudgetDiffAmount = totalBudgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); html += QString("").arg(showColoredAmount(totalBudgetAmount, totalBudgetValue.isNegative())); html += QString("").arg(showColoredAmount(totalActualAmount, totalActualValue.isNegative())); html += QString("").arg(showColoredAmount(totalBudgetDiffAmount, totalBudgetDiffValue.isNegative())); html += ""; html += "
"; html += i18n("Current Month Summary"); html += "
"; html += i18n("Budgeted"); html += ""; html += i18n("Actual"); html += ""; html += i18n("Difference"); html += "
%1%1%1
"; //budget overrun html += "
 
\n"; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; reports::PivotGrid::iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { i = 0; reports::PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { reports::PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { //column number is 1 because the report includes only current month if (it_row.value()[reports::eBudgetDiff].value(1).isNegative()) { //get report account to get the name later reports::ReportAccount rowname = it_row.key(); //write the outergroup if it is the first row of outergroup being shown if (i == 0) { html += ""; html += QString("").arg(MyMoneyAccount::accountTypeToString(rowname.accountType())); html += ""; } html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //get values from grid MyMoneyMoney actualValue = it_row.value()[reports::eActual][1]; MyMoneyMoney budgetValue = it_row.value()[reports::eBudget][1]; MyMoneyMoney budgetDiffValue = it_row.value()[reports::eBudgetDiff][1]; //format amounts QString actualAmount = actualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString budgetAmount = budgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString budgetDiffAmount = budgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); //account name html += QString(""; //show amounts html += QString("").arg(showColoredAmount(budgetAmount, budgetValue.isNegative())); html += QString("").arg(showColoredAmount(actualAmount, actualValue.isNegative())); html += QString("").arg(showColoredAmount(budgetDiffAmount, budgetDiffValue.isNegative())); html += ""; //set the flag that there are overruns isOverrun = true; } ++it_row; } ++it_innergroup; } ++it_outergroup; } //if no negative differences are found, then inform that - if (!isOverrun) { + if (isOverrun == false) { html += QString::fromLatin1("").arg(((i++ & 1) == 1) ? QLatin1String("even") : QLatin1String("odd")); html += QString::fromLatin1("").arg(i18n("No Budget Categories have been overrun")); html += ""; } html += "
"; html += i18n("Budget Overruns"); html += "
"; html += i18n("Account"); html += ""; html += i18n("Budgeted"); html += ""; html += i18n("Actual"); html += ""; html += i18n("Difference"); html += "
%1
") + link(VIEW_LEDGER, QString("?id=%1").arg(rowname.id()), QString()) + rowname.name() + linkend() + "%1%1%1
%1
"; return html; } QString ReportsView::showColoredAmount(const QString &amount, bool isNegative) const { 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; } QString ReportsView::link(const QString& view, const QString& query, const QString& _title) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString ReportsView::linkend() const { return QStringLiteral(""); } K_PLUGIN_FACTORY_WITH_JSON(ReportsViewFactory, "reportsview.json", registerPlugin();) #include "reportsview.moc" diff --git a/kmymoney/plugins/xml/xmlstorage.h b/kmymoney/plugins/xml/xmlstorage.h index 081a1c3c7..865f82797 100644 --- a/kmymoney/plugins/xml/xmlstorage.h +++ b/kmymoney/plugins/xml/xmlstorage.h @@ -1,83 +1,81 @@ /* * Copyright 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 XMLSTORAGE_H #define XMLSTORAGE_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes #include // Project Includes #include "kmymoneyplugin.h" class QIODevice; class MyMoneyStorageMgr; class XMLStorage : public KMyMoneyPlugin::Plugin, public KMyMoneyPlugin::StoragePlugin { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::StoragePlugin) public: explicit XMLStorage(QObject *parent, const QVariantList &args); ~XMLStorage() override; - QAction *m_saveAsXMLaction; - MyMoneyStorageMgr *open(const QUrl &url) override; bool save(const QUrl &url) override; bool saveAs() override; eKMyMoney::StorageType storageType() const override; QString fileExtension() const override; QUrl openUrl() const override; private: void createActions(); void ungetString(QIODevice *qfile, char *buf, int len); /** * This method is used by saveFile() to store the data * either directly in the destination file if it is on * the local file system or in a temporary file when * the final destination is reached over a network * protocol (e.g. FTP) * * @param localFile the name of the local file * @param writer pointer to the formatter * @param plaintext whether to override any compression & encryption settings * @param keyList QString containing a comma separated list of keys to be used for encryption * If @p keyList is empty, the file will be saved unencrypted * * @note This method will close the file when it is written. */ void saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList); void checkRecoveryKeyValidity(); QString m_encryptionKeys; QUrl fileUrl; }; #endif diff --git a/kmymoney/widgets/groupmarkers.cpp b/kmymoney/widgets/groupmarkers.cpp index 73acef8fd..2ed95e30b 100644 --- a/kmymoney/widgets/groupmarkers.cpp +++ b/kmymoney/widgets/groupmarkers.cpp @@ -1,240 +1,238 @@ /* * 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 "groupmarkers.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "groupmarker.h" #include "groupmarker_p.h" #include "itemptrvector.h" #include "mymoneyaccount.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace eWidgets; using namespace eMyMoney; namespace KMyMoneyRegister { class TypeGroupMarkerPrivate : public GroupMarkerPrivate { public: eRegister::CashFlowDirection m_dir; }; } TypeGroupMarker::TypeGroupMarker(KMyMoneyRegister::Register* parent, eRegister::CashFlowDirection dir, Account::Type accType) : GroupMarker(*new TypeGroupMarkerPrivate, parent, QString()) { Q_D(TypeGroupMarker); d->m_dir = dir; switch (dir) { case eRegister::CashFlowDirection::Deposit: d->m_txt = i18nc("Deposits onto account", "Deposits"); if (accType == Account::Type::CreditCard) { d->m_txt = i18nc("Payments towards credit card", "Payments"); } break; case eRegister::CashFlowDirection::Payment: d->m_txt = i18nc("Payments made from account", "Payments"); if (accType == Account::Type::CreditCard) { d->m_txt = i18nc("Payments made with credit card", "Charges"); } break; default: qDebug("Unknown CashFlowDirection %d for TypeGroupMarker constructor", (int)dir); break; } } TypeGroupMarker::~TypeGroupMarker() { } eRegister::CashFlowDirection TypeGroupMarker::sortType() const { Q_D(const TypeGroupMarker); return d->m_dir; } PayeeGroupMarker::PayeeGroupMarker(KMyMoneyRegister::Register* parent, const QString& name) : GroupMarker(parent, name) { } PayeeGroupMarker::~PayeeGroupMarker() { } const QString& PayeeGroupMarker::sortPayee() const { Q_D(const GroupMarker); return d->m_txt; } CategoryGroupMarker::CategoryGroupMarker(KMyMoneyRegister::Register* parent, const QString& category) : GroupMarker(parent, category) { } CategoryGroupMarker::~CategoryGroupMarker() { } const QString& CategoryGroupMarker::sortCategory() const { Q_D(const GroupMarker); return d->m_txt; } const QString CategoryGroupMarker::sortSecurity() const { Q_D(const GroupMarker); return d->m_txt; } const char* CategoryGroupMarker::className() { return "CategoryGroupMarker"; } namespace KMyMoneyRegister { class ReconcileGroupMarkerPrivate : public GroupMarkerPrivate { public: eMyMoney::Split::State m_state; }; } ReconcileGroupMarker::ReconcileGroupMarker(KMyMoneyRegister::Register* parent, eMyMoney::Split::State state) : GroupMarker(*new ReconcileGroupMarkerPrivate, parent, QString()) { Q_D(ReconcileGroupMarker); d->m_state = state; switch (state) { case eMyMoney::Split::State::NotReconciled: d->m_txt = i18nc("Reconcile state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: d->m_txt = i18nc("Reconcile state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: d->m_txt = i18nc("Reconcile state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: d->m_txt = i18nc("Reconcile state 'Frozen'", "Frozen"); break; default: d->m_txt = i18nc("Unknown reconcile state", "Unknown"); break; } } ReconcileGroupMarker::~ReconcileGroupMarker() { } eMyMoney::Split::State ReconcileGroupMarker::sortReconcileState() const { Q_D(const ReconcileGroupMarker); return d->m_state; } namespace KMyMoneyRegister { class RegisterPrivate { public: RegisterPrivate() : m_selectAnchor(nullptr), m_focusItem(nullptr), m_ensureVisibleItem(nullptr), m_firstItem(nullptr), m_lastItem(nullptr), m_firstErroneous(nullptr), m_lastErroneous(nullptr), - m_markErroneousTransactions(0), m_rowHeightHint(0), m_ledgerLensForced(false), m_selectionMode(QTableWidget::MultiSelection), m_needResize(true), m_listsDirty(false), m_ignoreNextButtonRelease(false), m_needInitialColumnResize(false), m_usedWithEditor(false), m_mouseButton(Qt::MouseButtons(Qt::NoButton)), m_modifiers(Qt::KeyboardModifiers(Qt::NoModifier)), m_lastCol(eTransaction::Column::Account), m_detailsColumnType(eRegister::DetailColumn::PayeeFirst) { } ~RegisterPrivate() { } ItemPtrVector m_items; QVector m_itemIndex; RegisterItem* m_selectAnchor; RegisterItem* m_focusItem; RegisterItem* m_ensureVisibleItem; RegisterItem* m_firstItem; RegisterItem* m_lastItem; RegisterItem* m_firstErroneous; RegisterItem* m_lastErroneous; - int m_markErroneousTransactions; int m_rowHeightHint; MyMoneyAccount m_account; bool m_ledgerLensForced; QAbstractItemView::SelectionMode m_selectionMode; bool m_needResize; bool m_listsDirty; bool m_ignoreNextButtonRelease; bool m_needInitialColumnResize; bool m_usedWithEditor; Qt::MouseButtons m_mouseButton; Qt::KeyboardModifiers m_modifiers; eTransaction::Column m_lastCol; QList m_sortOrder; QRect m_lastRepaintRect; eRegister::DetailColumn m_detailsColumnType; }; } diff --git a/kmymoney/widgets/register.cpp b/kmymoney/widgets/register.cpp index 8d32a299f..eedce9f43 100644 --- a/kmymoney/widgets/register.cpp +++ b/kmymoney/widgets/register.cpp @@ -1,1893 +1,1892 @@ /* * 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 "register.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "mymoneyaccount.h" #include "stdtransactiondownloaded.h" #include "stdtransactionmatched.h" #include "selectedtransactions.h" #include "scheduledtransaction.h" #include "kmymoneysettings.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "groupmarkers.h" #include "fancydategroupmarkers.h" #include "registeritemdelegate.h" #include "itemptrvector.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace eWidgets; using namespace eMyMoney; namespace KMyMoneyRegister { class RegisterPrivate { public: RegisterPrivate() : m_selectAnchor(nullptr), m_focusItem(nullptr), m_ensureVisibleItem(nullptr), m_firstItem(nullptr), m_lastItem(nullptr), m_firstErroneous(nullptr), m_lastErroneous(nullptr), m_rowHeightHint(0), m_ledgerLensForced(false), m_selectionMode(QTableWidget::MultiSelection), m_needResize(true), m_listsDirty(false), m_ignoreNextButtonRelease(false), m_needInitialColumnResize(false), m_usedWithEditor(false), m_mouseButton(Qt::MouseButtons(Qt::NoButton)), m_modifiers(Qt::KeyboardModifiers(Qt::NoModifier)), m_lastCol(eTransaction::Column::Account), m_detailsColumnType(eRegister::DetailColumn::PayeeFirst) { } ~RegisterPrivate() { } ItemPtrVector m_items; QVector m_itemIndex; RegisterItem* m_selectAnchor; RegisterItem* m_focusItem; RegisterItem* m_ensureVisibleItem; RegisterItem* m_firstItem; RegisterItem* m_lastItem; RegisterItem* m_firstErroneous; RegisterItem* m_lastErroneous; - int m_markErroneousTransactions; int m_rowHeightHint; MyMoneyAccount m_account; bool m_ledgerLensForced; QAbstractItemView::SelectionMode m_selectionMode; bool m_needResize; bool m_listsDirty; bool m_ignoreNextButtonRelease; bool m_needInitialColumnResize; bool m_usedWithEditor; Qt::MouseButtons m_mouseButton; Qt::KeyboardModifiers m_modifiers; eTransaction::Column m_lastCol; QList m_sortOrder; QRect m_lastRepaintRect; eRegister::DetailColumn m_detailsColumnType; }; Register::Register(QWidget *parent) : TransactionEditorContainer(parent), d_ptr(new RegisterPrivate) { // used for custom coloring with the help of the application's stylesheet setObjectName(QLatin1String("register")); setItemDelegate(new RegisterItemDelegate(this)); setEditTriggers(QAbstractItemView::NoEditTriggers); setColumnCount((int)eTransaction::Column::LastColumn); setSelectionBehavior(QAbstractItemView::SelectRows); setAcceptDrops(true); setShowGrid(false); setContextMenuPolicy(Qt::DefaultContextMenu); setHorizontalHeaderItem((int)eTransaction::Column::Number, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Date, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Account, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Security, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Detail, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::ReconcileFlag, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Payment, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Deposit, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Quantity, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Price, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Value, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Balance, new QTableWidgetItem()); // keep the following list in sync with KMyMoneyRegister::Column in transaction.h horizontalHeaderItem((int)eTransaction::Column::Number)->setText(i18nc("Cheque Number", "No.")); horizontalHeaderItem((int)eTransaction::Column::Date)->setText(i18n("Date")); horizontalHeaderItem((int)eTransaction::Column::Account)->setText(i18n("Account")); horizontalHeaderItem((int)eTransaction::Column::Security)->setText(i18n("Security")); horizontalHeaderItem((int)eTransaction::Column::Detail)->setText(i18n("Details")); horizontalHeaderItem((int)eTransaction::Column::ReconcileFlag)->setText(i18n("C")); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Deposit")); horizontalHeaderItem((int)eTransaction::Column::Quantity)->setText(i18n("Quantity")); horizontalHeaderItem((int)eTransaction::Column::Price)->setText(i18n("Price")); horizontalHeaderItem((int)eTransaction::Column::Value)->setText(i18n("Value")); horizontalHeaderItem((int)eTransaction::Column::Balance)->setText(i18n("Balance")); verticalHeader()->hide(); horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); horizontalHeader()->setSortIndicatorShown(false); horizontalHeader()->setSectionsMovable(false); horizontalHeader()->setSectionsClickable(false); horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QTableWidget::cellClicked, this, static_cast(&Register::selectItem)); connect(this, &QTableWidget::cellDoubleClicked, this, &Register::slotDoubleClicked); } Register::~Register() { Q_D(Register); clear(); delete d; } bool Register::eventFilter(QObject* o, QEvent* e) { if (o == this && e->type() == QEvent::KeyPress) { auto ke = dynamic_cast(e); if (ke && ke->key() == Qt::Key_Menu) { emit openContextMenu(); return true; } } return QTableWidget::eventFilter(o, e); } void Register::setupRegister(const MyMoneyAccount& account, const QList& cols) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); d->m_needInitialColumnResize = true; d->m_lastCol = static_cast(0); QList::const_iterator it_c; for (it_c = cols.begin(); it_c != cols.end(); ++it_c) { if ((*it_c) > eTransaction::Column::LastColumn) continue; showColumn((int)*it_c); if (*it_c > d->m_lastCol) d->m_lastCol = *it_c; } setUpdatesEnabled(true); } void Register::setupRegister(const MyMoneyAccount& account, bool showAccountColumn) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made from account", "Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Deposit into account", "Deposit")); if (account.id().isEmpty()) { setUpdatesEnabled(true); return; } d->m_needInitialColumnResize = true; // turn on standard columns showColumn((int)eTransaction::Column::Date); showColumn((int)eTransaction::Column::Detail); showColumn((int)eTransaction::Column::ReconcileFlag); // balance switch (account.accountType()) { case Account::Type::Stock: break; default: showColumn((int)eTransaction::Column::Balance); break; } // Number column switch (account.accountType()) { case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Loan: case Account::Type::AssetLoan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Equity: if (KMyMoneySettings::alwaysShowNrField()) showColumn((int)eTransaction::Column::Number); break; case Account::Type::Checkings: case Account::Type::CreditCard: showColumn((int)eTransaction::Column::Number); break; default: hideColumn((int)eTransaction::Column::Number); break; } switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: showAccountColumn = true; break; default: break; } if (showAccountColumn) showColumn((int)eTransaction::Column::Account); // Security, activity, payment, deposit, amount, price and value column switch (account.accountType()) { default: showColumn((int)eTransaction::Column::Payment); showColumn((int)eTransaction::Column::Deposit); break; case Account::Type::Investment: showColumn((int)eTransaction::Column::Security); showColumn((int)eTransaction::Column::Quantity); showColumn((int)eTransaction::Column::Price); showColumn((int)eTransaction::Column::Value); break; } // headings switch (account.accountType()) { case Account::Type::CreditCard: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made with credit card", "Charge")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Payment towards credit card", "Payment")); break; case Account::Type::Asset: case Account::Type::AssetLoan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Decrease of asset/liability value", "Decrease")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Increase of asset/liability value", "Increase")); break; case Account::Type::Liability: case Account::Type::Loan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Increase of asset/liability value", "Increase")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Decrease of asset/liability value", "Decrease")); break; case Account::Type::Income: case Account::Type::Expense: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Income")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Expense")); break; default: break; } d->m_lastCol = eTransaction::Column::Balance; setUpdatesEnabled(true); } bool Register::focusNextPrevChild(bool next) { return QFrame::focusNextPrevChild(next); } void Register::setSortOrder(const QString& order) { Q_D(Register); const QStringList orderList = order.split(',', QString::SkipEmptyParts); QStringList::const_iterator it; d->m_sortOrder.clear(); for (it = orderList.constBegin(); it != orderList.constEnd(); ++it) { d->m_sortOrder << static_cast((*it).toInt()); } } const QList& Register::sortOrder() const { Q_D(const Register); return d->m_sortOrder; } void Register::sortItems() { Q_D(Register); if (d->m_items.count() == 0) return; // sort the array of pointers to the transactions d->m_items.sort(); // update the next/prev item chains RegisterItem* prev = 0; RegisterItem* item; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; } // update the balance visibility settings item = d->m_lastItem; bool showBalance = true; while (item) { auto t = dynamic_cast(item); if (t) { t->setShowBalance(showBalance); if (!t->isVisible()) { showBalance = false; } } item = item->prevItem(); } // force update of the item index (row to item array) d->m_listsDirty = true; } eTransaction::Column Register::lastCol() const { Q_D(const Register); return d->m_lastCol; } SortField Register::primarySortKey() const { Q_D(const Register); if (!d->m_sortOrder.isEmpty()) return static_cast(d->m_sortOrder.first()); return SortField::Unknown; } void Register::clear() { Q_D(Register); d->m_firstErroneous = d->m_lastErroneous = 0; d->m_ensureVisibleItem = 0; d->m_items.clear(); RegisterItem* p; while ((p = firstItem()) != 0) { delete p; } d->m_firstItem = d->m_lastItem = 0; d->m_listsDirty = true; d->m_selectAnchor = 0; d->m_focusItem = 0; #ifndef KMM_DESIGNER // recalculate row height hint QFontMetrics fm(KMyMoneySettings::listCellFontEx()); d->m_rowHeightHint = fm.lineSpacing() + 6; #endif d->m_needInitialColumnResize = true; d->m_needResize = true; updateRegister(true); } void Register::insertItemAfter(RegisterItem*p, RegisterItem* prev) { Q_D(Register); RegisterItem* next = 0; if (!prev) prev = lastItem(); if (prev) { next = prev->nextItem(); prev->setNextItem(p); } if (next) next->setPrevItem(p); p->setPrevItem(prev); p->setNextItem(next); if (!d->m_firstItem) d->m_firstItem = p; if (!d->m_lastItem) d->m_lastItem = p; if (prev == d->m_lastItem) d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::addItem(RegisterItem* p) { Q_D(Register); RegisterItem* q = lastItem(); if (q) q->setNextItem(p); p->setPrevItem(q); p->setNextItem(0); d->m_items.append(p); if (!d->m_firstItem) d->m_firstItem = p; d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::removeItem(RegisterItem* p) { Q_D(Register); // remove item from list if (p->prevItem()) p->prevItem()->setNextItem(p->nextItem()); if (p->nextItem()) p->nextItem()->setPrevItem(p->prevItem()); // update first and last pointer if required if (p == d->m_firstItem) d->m_firstItem = p->nextItem(); if (p == d->m_lastItem) d->m_lastItem = p->prevItem(); // make sure we don't do it twice p->setNextItem(0); p->setPrevItem(0); // remove it from the m_items array int i = d->m_items.indexOf(p); if (-1 != i) { d->m_items[i] = 0; } d->m_listsDirty = true; d->m_needResize = true; } RegisterItem* Register::firstItem() const { Q_D(const Register); return d->m_firstItem; } RegisterItem* Register::nextItem(RegisterItem* item) const { return item->nextItem(); } RegisterItem* Register::lastItem() const { Q_D(const Register); return d->m_lastItem; } void Register::setupItemIndex(int rowCount) { Q_D(Register); // setup index array d->m_itemIndex.clear(); d->m_itemIndex.reserve(rowCount); // fill index array rowCount = 0; RegisterItem* prev = 0; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; for (int j = item->numRowsRegister(); j; --j) { d->m_itemIndex.push_back(item); } } } void Register::updateAlternate() const { Q_D(const Register); bool alternate = false; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->isVisible()) { item->setAlternate(alternate); alternate ^= true; } } } void Register::suppressAdjacentMarkers() { bool lastWasGroupMarker = false; KMyMoneyRegister::RegisterItem* p = lastItem(); auto t = dynamic_cast(p); if (t && t->transaction().id().isEmpty()) { lastWasGroupMarker = true; p = p->prevItem(); } while (p) { auto m = dynamic_cast(p); if (m) { // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->setVisible(false); } lastWasGroupMarker = true; } else if (p->isVisible()) lastWasGroupMarker = false; p = p->prevItem(); } } void Register::updateRegister(bool forceUpdateRowHeight) { Q_D(Register); if (d->m_listsDirty || forceUpdateRowHeight) { // don't get in here recursively d->m_listsDirty = false; int rowCount = 0; // determine the number of rows we need to display all items // while going through the list, check for erroneous transactions for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; item->setStartRow(rowCount); item->setNeedResize(); rowCount += item->numRowsRegister(); if (item->isErroneous()) { if (!d->m_firstErroneous) d->m_firstErroneous = item; d->m_lastErroneous = item; } } updateAlternate(); // create item index setupItemIndex(rowCount); bool needUpdateHeaders = (QTableWidget::rowCount() != rowCount) | forceUpdateRowHeight; // setup QTable. Make sure to suppress screen updates for now setRowCount(rowCount); // if we need to update the headers, we do it now for all rows // again we make sure to suppress screen updates if (needUpdateHeaders) { for (auto i = 0; i < rowCount; ++i) { RegisterItem* item = itemAtRow(i); if (item->isVisible()) { showRow(i); } else { hideRow(i); } verticalHeader()->resizeSection(i, item->rowHeightHint()); } verticalHeader()->setUpdatesEnabled(true); } // force resizeing of the columns if necessary if (d->m_needInitialColumnResize) { QTimer::singleShot(0, this, SLOT(resize())); d->m_needInitialColumnResize = false; } else { update(); // if the number of rows changed, we might need to resize the register // to make sure we reflect the current visibility of the scrollbars. if (needUpdateHeaders) QTimer::singleShot(0, this, SLOT(resize())); } } } int Register::rowHeightHint() const { Q_D(const Register); if (!d->m_rowHeightHint) { qDebug("Register::rowHeightHint(): m_rowHeightHint is zero!!"); } return d->m_rowHeightHint; } void Register::focusInEvent(QFocusEvent* ev) { Q_D(const Register); QTableWidget::focusInEvent(ev); if (d->m_focusItem) { d->m_focusItem->setFocus(true, false); } } bool Register::event(QEvent* event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); // get the row, if it's the header, then we're done // otherwise, adjust the row to be 0 based. int row = rowAt(helpEvent->y()); if (!row) return true; --row; int col = columnAt(helpEvent->x()); RegisterItem* item = itemAtRow(row); if (!item) return true; row = row - item->startRow(); QString msg; QRect rect; if (!item->maybeTip(helpEvent->pos(), row, col, rect, msg)) return true; if (!msg.isEmpty()) { QToolTip::showText(helpEvent->globalPos(), msg); } else { QToolTip::hideText(); event->ignore(); } return true; } return TransactionEditorContainer::event(event); } void Register::focusOutEvent(QFocusEvent* ev) { Q_D(Register); if (d->m_focusItem) { d->m_focusItem->setFocus(false, false); } QTableWidget::focusOutEvent(ev); } void Register::resizeEvent(QResizeEvent* ev) { TransactionEditorContainer::resizeEvent(ev); resize((int)eTransaction::Column::Detail, true); } void Register::resize() { resize((int)eTransaction::Column::Detail); } void Register::resize(int col, bool force) { Q_D(Register); if (!d->m_needResize && !force) return; d->m_needResize = false; // resize the register int w = viewport()->width(); // TODO I was playing a bit with manual ledger resizing but could not get // a good solution. I just leave the code around, so that maybe others // pick it up again. So far, it's not clear to me where to store the // size of the sections: // // a) with the account (as it is done now) // b) with the application for the specific account type // c) ???? // // Ideas are welcome (ipwizard: 2007-07-19) // Note: currently there's no way to switch back to automatic // column sizing once the manual sizing option has been saved #if 0 if (m_account.value("kmm-ledger-column-width").isEmpty()) { #endif // check which space we need if (columnWidth((int)eTransaction::Column::Number)) adjustColumn((int)eTransaction::Column::Number); if (columnWidth((int)eTransaction::Column::Account)) adjustColumn((int)eTransaction::Column::Account); if (columnWidth((int)eTransaction::Column::Payment)) adjustColumn((int)eTransaction::Column::Payment); if (columnWidth((int)eTransaction::Column::Deposit)) adjustColumn((int)eTransaction::Column::Deposit); if (columnWidth((int)eTransaction::Column::Quantity)) adjustColumn((int)eTransaction::Column::Quantity); if (columnWidth((int)eTransaction::Column::Balance)) adjustColumn((int)eTransaction::Column::Balance); if (columnWidth((int)eTransaction::Column::Price)) adjustColumn((int)eTransaction::Column::Price); if (columnWidth((int)eTransaction::Column::Value)) adjustColumn((int)eTransaction::Column::Value); // make amount columns all the same size // only extend the entry columns to make sure they fit // the widget int dwidth = 0; int ewidth = 0; if (ewidth < columnWidth((int)eTransaction::Column::Payment)) ewidth = columnWidth((int)eTransaction::Column::Payment); if (ewidth < columnWidth((int)eTransaction::Column::Deposit)) ewidth = columnWidth((int)eTransaction::Column::Deposit); if (ewidth < columnWidth((int)eTransaction::Column::Quantity)) ewidth = columnWidth((int)eTransaction::Column::Quantity); if (dwidth < columnWidth((int)eTransaction::Column::Balance)) dwidth = columnWidth((int)eTransaction::Column::Balance); if (ewidth < columnWidth((int)eTransaction::Column::Price)) ewidth = columnWidth((int)eTransaction::Column::Price); if (dwidth < columnWidth((int)eTransaction::Column::Value)) dwidth = columnWidth((int)eTransaction::Column::Value); int swidth = columnWidth((int)eTransaction::Column::Security); if (swidth > 0) { adjustColumn((int)eTransaction::Column::Security); swidth = columnWidth((int)eTransaction::Column::Security); } adjustColumn((int)eTransaction::Column::Date); #ifndef KMM_DESIGNER // Resize the date and money fields to either // a) the size required by the input widget if no transaction form is shown and the register is used with an editor // b) the adjusted value for the input widget if the transaction form is visible or an editor is not used if (d->m_usedWithEditor && !KMyMoneySettings::transactionForm()) { QPushButton *pushButton = new QPushButton; const int pushButtonSpacing = pushButton->sizeHint().width() + 5; setColumnWidth((int)eTransaction::Column::Date, columnWidth((int)eTransaction::Column::Date) + pushButtonSpacing + 4/* space for the spinbox arrows */); ewidth += pushButtonSpacing; if (swidth > 0) { // extend the security width to make space for the selector arrow swidth = columnWidth((int)eTransaction::Column::Security) + 40; } delete pushButton; } #endif if (columnWidth((int)eTransaction::Column::Payment)) setColumnWidth((int)eTransaction::Column::Payment, ewidth); if (columnWidth((int)eTransaction::Column::Deposit)) setColumnWidth((int)eTransaction::Column::Deposit, ewidth); if (columnWidth((int)eTransaction::Column::Quantity)) setColumnWidth((int)eTransaction::Column::Quantity, ewidth); if (columnWidth((int)eTransaction::Column::Balance)) setColumnWidth((int)eTransaction::Column::Balance, dwidth); if (columnWidth((int)eTransaction::Column::Price)) setColumnWidth((int)eTransaction::Column::Price, ewidth); if (columnWidth((int)eTransaction::Column::Value)) setColumnWidth((int)eTransaction::Column::Value, dwidth); if (columnWidth((int)eTransaction::Column::ReconcileFlag)) setColumnWidth((int)eTransaction::Column::ReconcileFlag, 20); if (swidth > 0) setColumnWidth((int)eTransaction::Column::Security, swidth); #if 0 // see comment above } else { QStringList colSizes = QStringList::split(",", m_account.value("kmm-ledger-column-width"), true); for (int i; i < colSizes.count(); ++i) { int colWidth = colSizes[i].toInt(); if (colWidth == 0) continue; setColumnWidth(i, w * colWidth / 100); } } #endif for (auto i = 0; i < columnCount(); ++i) { if (i == col) continue; w -= columnWidth(i); } setColumnWidth(col, w); } void Register::forceUpdateLists() { Q_D(Register); d->m_listsDirty = true; } int Register::minimumColumnWidth(int col) { Q_D(Register); QHeaderView *topHeader = horizontalHeader(); int w = topHeader->fontMetrics().width(horizontalHeaderItem(col) ? horizontalHeaderItem(col)->text() : QString()) + 10; w = qMax(w, 20); #ifdef KMM_DESIGNER return w; #else int maxWidth = 0; int minWidth = 0; QFontMetrics cellFontMetrics(KMyMoneySettings::listCellFontEx()); switch (col) { case (int)eTransaction::Column::Date: minWidth = cellFontMetrics.width(QLocale().toString(QDate(6999, 12, 29), QLocale::ShortFormat) + " "); break; default: break; } // scan through the transactions for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; auto t = dynamic_cast(item); if (t) { int nw = 0; try { nw = t->registerColWidth(col, cellFontMetrics); } catch (const MyMoneyException &) { // This should only be reached if the data in the file disappeared // from under us, such as when the account was deleted from a // different view, then this view is restored. In this case, new // data is about to be loaded into the view anyway, so just remove // the item from the register and swallow the exception. //qDebug("%s", e.what()); removeItem(t); } w = qMax(w, nw); if (maxWidth) { if (w > maxWidth) { w = maxWidth; break; } } if (minWidth && (w < minWidth)) { w = minWidth; } } } return w; #endif } void Register::adjustColumn(int col) { setColumnWidth(col, minimumColumnWidth(col)); } void Register::clearSelection() { unselectItems(); TransactionEditorContainer::clearSelection(); } void Register::doSelectItems(int from, int to, bool selected) { Q_D(Register); int start, end; // make sure start is smaller than end if (from <= to) { start = from; end = to; } else { start = to; end = from; } // make sure we stay in bounds if (start < 0) start = 0; if ((end <= -1) || (end > (d->m_items.size() - 1))) end = d->m_items.size() - 1; RegisterItem* firstItem; RegisterItem* lastItem; firstItem = lastItem = 0; for (int i = start; i <= end; ++i) { RegisterItem* const item = d->m_items[i]; if (item) { if (selected != item->isSelected()) { if (!firstItem) firstItem = item; item->setSelected(selected); lastItem = item; } } } } RegisterItem* Register::itemAtRow(int row) const { Q_D(const Register); if (row >= 0 && row < d->m_itemIndex.size()) { return d->m_itemIndex[row]; } return 0; } int Register::rowToIndex(int row) const { Q_D(const Register); for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; if (row >= item->startRow() && row < (item->startRow() + item->numRowsRegister())) return i; } return -1; } void Register::selectedTransactions(SelectedTransactions& list) const { Q_D(const Register); if (d->m_focusItem && d->m_focusItem->isSelected() && d->m_focusItem->isVisible()) { auto t = dynamic_cast(d->m_focusItem); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; // make sure, we don't include the focus item twice if (item == d->m_focusItem) continue; if (item && item->isSelected() && item->isVisible()) { auto t = dynamic_cast(item); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } } } QList Register::selectedItems() const { Q_D(const Register); QList list; RegisterItem* item = d->m_firstItem; while (item) { if (item && item->isSelected() && item->isVisible()) { list << item; } item = item->nextItem(); } return list; } int Register::selectedItemsCount() const { Q_D(const Register); auto cnt = 0; RegisterItem* item = d->m_firstItem; while (item) { if (item->isSelected() && item->isVisible()) ++cnt; item = item->nextItem(); } return cnt; } void Register::mouseReleaseEvent(QMouseEvent *e) { Q_D(Register); if (e->button() == Qt::RightButton) { // see the comment in Register::contextMenuEvent // on Linux we never get here but on Windows this // event is fired before the contextMenuEvent which // causes the loss of the multiple selection; to avoid // this just ignore the event and act like on Linux return; } if (d->m_ignoreNextButtonRelease) { d->m_ignoreNextButtonRelease = false; return; } d->m_mouseButton = e->button(); d->m_modifiers = QApplication::keyboardModifiers(); QTableWidget::mouseReleaseEvent(e); } void Register::contextMenuEvent(QContextMenuEvent *e) { Q_D(Register); if (e->reason() == QContextMenuEvent::Mouse) { // since mouse release event is not called, we need // to reset the mouse button and the modifiers here d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; // if a selected item is clicked don't change the selection RegisterItem* item = itemAtRow(rowAt(e->y())); if (item && !item->isSelected()) selectItem(rowAt(e->y()), columnAt(e->x())); } openContextMenu(); } void Register::unselectItems(int from, int to) { doSelectItems(from, to, false); } void Register::selectItems(int from, int to) { doSelectItems(from, to, true); } void Register::selectItem(int row, int col) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* item = d->m_itemIndex[row]; // don't support selecting when the item has an editor // or the item itself is not selectable if (item->hasEditorOpen() || !item->isSelectable()) { d->m_mouseButton = Qt::NoButton; return; } QString id = item->id(); selectItem(item); // selectItem() might have changed the pointers, so we // need to reconstruct it here item = itemById(id); auto t = dynamic_cast(item); if (t) { if (!id.isEmpty()) { if (t && col == (int)eTransaction::Column::ReconcileFlag && selectedItemsCount() == 1 && !t->isScheduled()) emit reconcileStateColumnClicked(t); } else { emit emptyItemSelected(); } } } } void Register::setAnchorItem(RegisterItem* anchorItem) { Q_D(Register); d->m_selectAnchor = anchorItem; } bool Register::setFocusItem(RegisterItem* focusItem) { Q_D(Register); if (focusItem && focusItem->canHaveFocus()) { if (d->m_focusItem) { d->m_focusItem->setFocus(false); } auto item = dynamic_cast(focusItem); if (d->m_focusItem != focusItem && item) { emit focusChanged(item); } d->m_focusItem = focusItem; d->m_focusItem->setFocus(true); if (d->m_listsDirty) updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); ensureItemVisible(d->m_focusItem); return true; } else return false; } bool Register::setFocusToTop() { Q_D(Register); RegisterItem* rgItem = d->m_firstItem; while (rgItem) { if (setFocusItem(rgItem)) return true; rgItem = rgItem->nextItem(); } return false; } void Register::selectItem(RegisterItem* item, bool dontChangeSelections) { Q_D(Register); if (!item) return; Qt::MouseButtons buttonState = d->m_mouseButton; Qt::KeyboardModifiers modifiers = d->m_modifiers; d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; if (d->m_selectionMode == NoSelection) return; if (item->isSelectable()) { QString id = item->id(); QList itemList = selectedItems(); bool okToSelect = true; auto cnt = itemList.count(); auto scheduledTransactionSelected = false; if (cnt > 0) { auto& r = *(itemList.front()); scheduledTransactionSelected = (typeid(r) == typeid(StdTransactionScheduled)); } if (buttonState & Qt::LeftButton) { if (!(modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) || (d->m_selectAnchor == 0)) { if ((cnt != 1) || ((cnt == 1) && !item->isSelected())) { emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); item->setSelected(true); setFocusItem(item); } } if (okToSelect) d->m_selectAnchor = item; } if (d->m_selectionMode == MultiSelection) { switch (modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) { case Qt::ControlModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; // toggle selection state of current item emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); item->setSelected(!item->isSelected()); setFocusItem(item); } break; case Qt::ShiftModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); if (d->m_selectAnchor) selectItems(rowToIndex(d->m_selectAnchor->startRow()), rowToIndex(item->startRow())); setFocusItem(item); } break; } } } else { // we get here when called by application logic emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); if (!dontChangeSelections) unselectItems(); item->setSelected(true); setFocusItem(item); d->m_selectAnchor = item; } } if (okToSelect) { SelectedTransactions list(this); emit transactionsSelected(list); } } } void Register::ensureFocusItemVisible() { Q_D(Register); ensureItemVisible(d->m_focusItem); } void Register::ensureItemVisible(RegisterItem* item) { Q_D(Register); if (!item) return; d->m_ensureVisibleItem = item; QTimer::singleShot(0, this, SLOT(slotEnsureItemVisible())); } void Register::slotDoubleClicked(int row, int) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* p = d->m_itemIndex[row]; if (p->isSelectable()) { d->m_ignoreNextButtonRelease = true; // double click to start editing only works if the focus // item is among the selected ones if (!focusItem()) { setFocusItem(p); if (d->m_selectionMode != NoSelection) p->setSelected(true); } if (d->m_focusItem->isSelected()) { // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); } } } } void Register::slotEnsureItemVisible() { Q_D(Register); // if clear() has been called since the timer was // started, we just ignore the call if (!d->m_ensureVisibleItem) return; // make sure to catch latest changes setUpdatesEnabled(false); updateRegister(); setUpdatesEnabled(true); // since the item will be made visible at the top of the viewport make the bottom index visible first to make the whole item visible scrollTo(model()->index(d->m_ensureVisibleItem->startRow() + d->m_ensureVisibleItem->numRowsRegister() - 1, (int)eTransaction::Column::Detail)); scrollTo(model()->index(d->m_ensureVisibleItem->startRow(), (int)eTransaction::Column::Detail)); } QString Register::text(int /*row*/, int /*col*/) const { return QString("a"); } QWidget* Register::createEditor(int /*row*/, int /*col*/, bool /*initFromCell*/) const { return 0; } void Register::setCellContentFromEditor(int /*row*/, int /*col*/) { } void Register::endEdit() { Q_D(Register); d->m_ignoreNextButtonRelease = false; } RegisterItem* Register::focusItem() const { Q_D(const Register); return d->m_focusItem; } RegisterItem* Register::anchorItem() const { Q_D(const Register); return d->m_selectAnchor; } void Register::arrangeEditWidgets(QMap& editWidgets, KMyMoneyRegister::Transaction* t) { t->arrangeWidgetsInRegister(editWidgets); ensureItemVisible(t); // updateContents(); } void Register::tabOrder(QWidgetList& tabOrderWidgets, KMyMoneyRegister::Transaction* t) const { t->tabOrderInRegister(tabOrderWidgets); } void Register::removeEditWidgets(QMap& editWidgets) { // remove pointers from map QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end();) { if ((*it)->parentWidget() == this) { editWidgets.erase(it); it = editWidgets.begin(); } else ++it; } // now delete the widgets if (auto t = dynamic_cast(focusItem())) { for (int row = t->startRow(); row < t->startRow() + t->numRowsRegister(true); ++row) { for (int col = 0; col < columnCount(); ++col) { if (cellWidget(row, col)) { cellWidget(row, col)->hide(); setCellWidget(row, col, 0); } } // make sure to reduce the possibly size to what it was before editing started setRowHeight(row, t->rowHeightHint()); } } } RegisterItem* Register::itemById(const QString& id) const { Q_D(const Register); if (id.isEmpty()) return d->m_lastItem; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->id() == id) return item; } return 0; } void Register::handleItemChange(RegisterItem* old, bool shift, bool control) { Q_D(Register); if (d->m_selectionMode == MultiSelection) { if (shift) { selectRange(d->m_selectAnchor ? d->m_selectAnchor : old, d->m_focusItem, false, true, (d->m_selectAnchor && !control) ? true : false); } else if (!control) { selectItem(d->m_focusItem, false); } } } void Register::selectRange(RegisterItem* from, RegisterItem* to, bool invert, bool includeFirst, bool clearSel) { if (!from || !to) return; if (from == to && !includeFirst) return; bool swap = false; if (to == from->prevItem()) swap = true; RegisterItem* item; if (!swap && from != to && from != to->prevItem()) { bool found = false; for (item = from; item; item = item->nextItem()) { if (item == to) { found = true; break; } } if (!found) swap = true; } if (swap) { item = from; from = to; to = item; if (!includeFirst) to = to->prevItem(); } else if (!includeFirst) { from = from->nextItem(); } if (clearSel) { for (item = firstItem(); item; item = item->nextItem()) { if (item->isSelected() && item->isVisible()) { item->setSelected(false); } } } for (item = from; item; item = item->nextItem()) { if (item->isSelectable()) { if (!invert) { if (!item->isSelected() && item->isVisible()) { item->setSelected(true); } } else { bool sel = !item->isSelected(); if ((item->isSelected() != sel) && item->isVisible()) { item->setSelected(sel); } } } if (item == to) break; } } void Register::scrollPage(int key, Qt::KeyboardModifiers modifiers) { Q_D(Register); RegisterItem* oldFocusItem = d->m_focusItem; // make sure we have a focus item if (!d->m_focusItem) setFocusItem(d->m_firstItem); if (!d->m_focusItem && d->m_firstItem) setFocusItem(d->m_firstItem->nextItem()); if (!d->m_focusItem) return; RegisterItem* item = d->m_focusItem; int height = 0; switch (key) { case Qt::Key_PageUp: while (height < viewport()->height() && item->prevItem()) { do { item = item->prevItem(); if (item->isVisible()) height += item->rowHeightHint(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); } break; case Qt::Key_PageDown: while (height < viewport()->height() && item->nextItem()) { do { if (item->isVisible()) height += item->rowHeightHint(); item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); } break; case Qt::Key_Up: if (item->prevItem()) { do { item = item->prevItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); } break; case Qt::Key_Down: if (item->nextItem()) { do { item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); } break; case Qt::Key_Home: item = d->m_firstItem; while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); break; case Qt::Key_End: item = d->m_lastItem; while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); break; } // make sure to avoid selecting a possible empty transaction at the end auto t = dynamic_cast(item); if (t && t->transaction().id().isEmpty()) { if (t->prevItem()) { item = t->prevItem(); } } if (!(modifiers & Qt::ShiftModifier) || !d->m_selectAnchor) d->m_selectAnchor = item; setFocusItem(item); if (item->isSelectable()) { handleItemChange(oldFocusItem, modifiers & Qt::ShiftModifier, modifiers & Qt::ControlModifier); // tell the world about the changes in selection SelectedTransactions list(this); emit transactionsSelected(list); } if (d->m_focusItem && !d->m_focusItem->isSelected() && d->m_selectionMode == SingleSelection) selectItem(item); } void Register::keyPressEvent(QKeyEvent* ev) { Q_D(Register); switch (ev->key()) { case Qt::Key_Space: if (d->m_selectionMode != NoSelection) { // get the state out of the event ... d->m_modifiers = ev->modifiers(); // ... and pretend that we have pressed the left mouse button ;) d->m_mouseButton = Qt::LeftButton; selectItem(d->m_focusItem); } break; case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Down: case Qt::Key_Up: scrollPage(ev->key(), ev->modifiers()); break; case Qt::Key_Enter: case Qt::Key_Return: // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); break; default: QTableWidget::keyPressEvent(ev); break; } } Transaction* Register::transactionFactory(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) { Transaction* t = 0; MyMoneySplit s = split; if (parent->account() == MyMoneyAccount()) { t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); return t; } switch (parent->account().accountType()) { 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::Currency: case Account::Type::Income: case Account::Type::Expense: case Account::Type::AssetLoan: case Account::Type::Equity: if (s.accountId().isEmpty()) s.setAccountId(parent->account().id()); if (s.isMatched()) t = new KMyMoneyRegister::StdTransactionMatched(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::StdTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); break; case Account::Type::Investment: if (s.isMatched()) t = new KMyMoneyRegister::InvestTransaction/* Matched */(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::InvestTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::InvestTransaction(parent, transaction, s, uniqueId); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Stock: default: qDebug("Register::transactionFactory: invalid accountTypeE %d", (int)parent->account().accountType()); break; } return t; } const MyMoneyAccount& Register::account() const { Q_D(const Register); return d->m_account; } void Register::addGroupMarkers() { Q_D(Register); QMap list; QMap::const_iterator it; KMyMoneyRegister::RegisterItem* p = firstItem(); KMyMoneyRegister::Transaction* t; QString name; QDate today; QDate yesterday, thisWeek, lastWeek; QDate thisMonth, lastMonth; QDate thisYear; int weekStartOfs; switch (primarySortKey()) { case SortField::PostDate: case SortField::EntryDate: today = QDate::currentDate(); thisMonth.setDate(today.year(), today.month(), 1); lastMonth = thisMonth.addMonths(-1); yesterday = today.addDays(-1); // a = QDate::dayOfWeek() todays weekday (1 = Monday, 7 = Sunday) // b = QLocale().firstDayOfWeek() first day of week (1 = Monday, 7 = Sunday) weekStartOfs = today.dayOfWeek() - QLocale().firstDayOfWeek(); if (weekStartOfs < 0) { weekStartOfs = 7 + weekStartOfs; } thisWeek = today.addDays(-weekStartOfs); lastWeek = thisWeek.addDays(-7); thisYear.setDate(today.year(), 1, 1); if (KMyMoneySettings::startDate().date() != QDate(1900, 1, 1)) new KMyMoneyRegister::FancyDateGroupMarker(this, KMyMoneySettings::startDate().date(), i18n("Prior transactions possibly filtered")); if (KMyMoneySettings::showFancyMarker()) { if (d->m_account.lastReconciliationDate().isValid()) new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.lastReconciliationDate(), i18n("Last reconciliation")); if (!d->m_account.value("lastImportedTransactionDate").isEmpty() && !d->m_account.value("lastStatementBalance").isEmpty()) { MyMoneyMoney balance(d->m_account.value("lastStatementBalance")); if (d->m_account.accountGroup() == Account::Type::Liability) balance = -balance; auto txt = i18n("Online Statement Balance: %1", balance.formatMoney(d->m_account.fraction())); KMyMoneyRegister::StatementGroupMarker *pGroupMarker = new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, QDate::fromString(d->m_account.value("lastImportedTransactionDate"), Qt::ISODate), txt); pGroupMarker->setErroneous(!MyMoneyFile::instance()->hasMatchingOnlineBalance(d->m_account)); } new KMyMoneyRegister::FancyDateGroupMarker(this, thisYear, i18n("This year")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastMonth, i18n("Last month")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth, i18n("This month")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastWeek, i18n("Last week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek, i18n("This week")); new KMyMoneyRegister::FancyDateGroupMarker(this, yesterday, i18n("Yesterday")); new KMyMoneyRegister::FancyDateGroupMarker(this, today, i18n("Today")); new KMyMoneyRegister::FancyDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek.addDays(7), i18n("Next week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth.addMonths(1), i18n("Next month")); } else { new KMyMoneyRegister::SimpleDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); } if (KMyMoneySettings::showFiscalMarker()) { QDate currentFiscalYear = KMyMoneySettings::firstFiscalDate(); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear, i18n("Current fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(-1), i18n("Previous fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(1), i18n("Next fiscal year")); } break; case SortField::Type: if (KMyMoneySettings::showFancyMarker()) { new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.accountType()); new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Payment, d->m_account.accountType()); } break; case SortField::ReconcileState: if (KMyMoneySettings::showFancyMarker()) { new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::NotReconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Cleared); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Reconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Frozen); } break; case SortField::Payee: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortPayee()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown payee", "Unknown"); } new KMyMoneyRegister::PayeeGroupMarker(this, name); } } break; case SortField::Category: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortCategory()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown category", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; case SortField::Security: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortSecurity()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown security", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; default: // no markers supported break; } } void Register::removeUnwantedGroupMarkers() { // remove all trailing group markers except statement markers KMyMoneyRegister::RegisterItem* q; KMyMoneyRegister::RegisterItem* p = lastItem(); while (p) { q = p; if (dynamic_cast(p) || dynamic_cast(p)) break; p = p->prevItem(); delete q; } // remove all adjacent group markers bool lastWasGroupMarker = false; p = lastItem(); while (p) { q = p; auto m = dynamic_cast(p); p = p->prevItem(); if (m) { m->markVisible(true); // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->markVisible(false); } lastWasGroupMarker = true; } else if (q->isVisible()) lastWasGroupMarker = false; } } void Register::setLedgerLensForced(bool forced) { Q_D(Register); d->m_ledgerLensForced = forced; } bool Register::ledgerLens() const { Q_D(const Register); return d->m_ledgerLensForced; } void Register::setSelectionMode(SelectionMode mode) { Q_D(Register); d->m_selectionMode = mode; } void Register::setUsedWithEditor(bool value) { Q_D(Register); d->m_usedWithEditor = value; } eRegister::DetailColumn Register::getDetailsColumnType() const { Q_D(const Register); return d->m_detailsColumnType; } void Register::setDetailsColumnType(eRegister::DetailColumn detailsColumnType) { Q_D(Register); d->m_detailsColumnType = detailsColumnType; } }