diff --git a/kmymoney/dialogs/knewbankdlg.cpp b/kmymoney/dialogs/knewbankdlg.cpp index 71b3d0d18..b5bee9070 100644 --- a/kmymoney/dialogs/knewbankdlg.cpp +++ b/kmymoney/dialogs/knewbankdlg.cpp @@ -1,260 +1,262 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2017 Ł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 "knewbankdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_knewbankdlg.h" #include "mymoneyinstitution.h" #include "kmymoneyutils.h" #include "icons.h" #include class KNewBankDlgPrivate { Q_DISABLE_COPY(KNewBankDlgPrivate) public: KNewBankDlgPrivate() : ui(new Ui::KNewBankDlg) { m_iconLoadTimer.setSingleShot(true); } ~KNewBankDlgPrivate() { delete ui; } Ui::KNewBankDlg* ui; MyMoneyInstitution m_institution; QTimer m_iconLoadTimer; QPointer m_favIconJob; QIcon m_favIcon; QString m_iconName; QUrl m_url; }; KNewBankDlg::KNewBankDlg(MyMoneyInstitution& institution, QWidget *parent) : QDialog(parent), d_ptr(new KNewBankDlgPrivate) { Q_D(KNewBankDlg); d->ui->setupUi(this); d->m_institution = institution; setModal(true); d->ui->nameEdit->setFocus(); d->ui->nameEdit->setText(institution.name()); d->ui->cityEdit->setText(institution.city()); d->ui->streetEdit->setText(institution.street()); d->ui->postcodeEdit->setText(institution.postcode()); d->ui->telephoneEdit->setText(institution.telephone()); d->ui->sortCodeEdit->setText(institution.sortcode()); d->ui->bicEdit->setText(institution.value(QStringLiteral("bic"))); d->ui->urlEdit->setText(institution.value(QStringLiteral("url"))); if (!institution.value(QStringLiteral("icon")).isEmpty()) { d->m_favIcon = Icons::loadIconFromApplicationCache(institution.value(QStringLiteral("icon"))); } if (!d->m_favIcon.isNull()) { d->ui->iconButton->setEnabled(true); d->ui->iconButton->setIcon(d->m_favIcon); } d->ui->messageWidget->hide(); connect(d->ui->buttonBox, &QDialogButtonBox::accepted, this, &KNewBankDlg::okClicked); connect(d->ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(d->ui->nameEdit, &QLineEdit::textChanged, this, &KNewBankDlg::institutionNameChanged); connect(d->ui->urlEdit, &QLineEdit::textChanged, this, &KNewBankDlg::slotUrlChanged); connect(&d->m_iconLoadTimer, &QTimer::timeout, this, &KNewBankDlg::slotLoadIcon); connect(d->ui->iconButton, &QToolButton::pressed, this, [=] { QUrl url; url.setUrl(QString::fromLatin1("https://%1/").arg(d->ui->urlEdit->text())); QDesktopServices::openUrl(url); }); institutionNameChanged(d->ui->nameEdit->text()); slotUrlChanged(d->ui->urlEdit->text()); auto requiredFields = new KMandatoryFieldGroup(this); requiredFields->setOkButton(d->ui->buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present requiredFields->add(d->ui->nameEdit); } void KNewBankDlg::institutionNameChanged(const QString &_text) { Q_D(KNewBankDlg); d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_text.isEmpty()); } KNewBankDlg::~KNewBankDlg() { Q_D(KNewBankDlg); delete d; } void KNewBankDlg::okClicked() { Q_D(KNewBankDlg); if (d->ui->nameEdit->text().isEmpty()) { KMessageBox::information(this, i18n("The institution name field is empty. Please enter the name."), i18n("Adding New Institution")); d->ui->nameEdit->setFocus(); return; } d->m_institution.setName(d->ui->nameEdit->text()); d->m_institution.setTown(d->ui->cityEdit->text()); d->m_institution.setStreet(d->ui->streetEdit->text()); d->m_institution.setPostcode(d->ui->postcodeEdit->text()); d->m_institution.setTelephone(d->ui->telephoneEdit->text()); d->m_institution.setSortcode(d->ui->sortCodeEdit->text()); d->m_institution.setValue(QStringLiteral("bic"), d->ui->bicEdit->text()); d->m_institution.setValue(QStringLiteral("url"), d->ui->urlEdit->text()); d->m_institution.deletePair(QStringLiteral("icon")); if (d->ui->iconButton->isEnabled()) { d->m_institution.setValue(QStringLiteral("icon"), d->m_iconName); Icons::storeIconInApplicationCache(d->m_iconName, d->m_favIcon); } accept(); } const MyMoneyInstitution& KNewBankDlg::institution() { Q_D(KNewBankDlg); return d->m_institution; } void KNewBankDlg::newInstitution(MyMoneyInstitution& institution) { institution.clearId(); QPointer dlg = new KNewBankDlg(institution); if (dlg->exec() == QDialog::Accepted && dlg != 0) { institution = dlg->institution(); KMyMoneyUtils::newInstitution(institution); } delete dlg; } void KNewBankDlg::slotUrlChanged(const QString& newUrl) { Q_D(KNewBankDlg); // remove a possible leading protocol since we only provide https for now QRegularExpression protocol(QStringLiteral("^[a-zA-Z]+://(?.*)"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch matcher = protocol.match(newUrl); if (matcher.hasMatch()) { d->ui->urlEdit->setText(matcher.captured(QStringLiteral("url"))); d->ui->messageWidget->setText(QLatin1String("The protocol part has been removed by KMyMoney because it is fixed to https.")); d->ui->messageWidget->setMessageType(KMessageWidget::Information); d->ui->messageWidget->animatedShow(); } d->m_iconLoadTimer.start(200); } void KNewBankDlg::slotLoadIcon() { Q_D(KNewBankDlg); // if currently a check is running, retry later if (d->m_favIconJob) { d->m_iconLoadTimer.start(200); return; } const auto path = d->ui->urlEdit->text(); QRegularExpression urlRe(QStringLiteral("^(.*\\.)?[^\\.]{2,}\\.[a-z]{2,}"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch matcher = urlRe.match(path); d->ui->iconButton->setEnabled(false); if (matcher.hasMatch()) { d->ui->iconButton->setEnabled(true); d->m_url = QUrl(QString::fromLatin1("https://%1").arg(path)); KIO::Scheduler::checkSlaveOnHold(true); d->m_favIconJob = new KIO::FavIconRequestJob(d->m_url); connect(d->m_favIconJob, &KIO::FavIconRequestJob::result, this, &KNewBankDlg::slotIconLoaded); // we force to end the job after 1 second to avoid blocking this mechanism in case the thing fails QTimer::singleShot(1000, this, &KNewBankDlg::killIconLoad); } } void KNewBankDlg::killIconLoad() { Q_D(KNewBankDlg); if (d->m_favIconJob) { d->m_favIconJob->kill(); d->m_favIconJob->deleteLater(); } } void KNewBankDlg::slotIconLoaded(KJob* job) { Q_D(KNewBankDlg); switch(job->error()) { case ECONNREFUSED: // There is an answer from the server, but no favicon. In case we // already have one, we keep it d->ui->iconButton->setEnabled(true); d->m_favIcon = Icons::get(Icons::Icon::ViewBank); d->m_iconName = QStringLiteral("enum:ViewBank"); break; case 0: // There is an answer from the server, and the favicon is found d->ui->iconButton->setEnabled(true); d->m_favIcon = QIcon(dynamic_cast(job)->iconFile()); d->m_iconName = QStringLiteral("favicon:%1").arg(d->m_url.host()); break; default: // There is problem with the URL from qDebug() << "KIO::FavIconRequestJob error" << job->error(); + // intentional fall through + case EALREADY: // invalid URL, no server response d->ui->iconButton->setEnabled(false); d->m_favIcon = QIcon(); d->m_iconName.clear(); break; } d->ui->iconButton->setIcon(d->m_favIcon); } diff --git a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp index d9ee6b068..7d84f93b2 100644 --- a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp +++ b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp @@ -1,787 +1,791 @@ /* * Copyright 2018 Ralf Habacker * Copyright 2018 Thomas Baumgart * * 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 "mymoneytransactionfilter-test.h" #include #include "mymoneyenums.h" #include "mymoneytransactionfilter.h" #include "mymoneyfile.h" #include "mymoneystoragemgr.h" #include "mymoneyaccount.h" #include "mymoneypayee.h" #include "mymoneysecurity.h" #include "mymoneytag.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyexception.h" // uses helper functions from reports tests #include "tests/testutilities.h" using namespace test; QTEST_GUILESS_MAIN(MyMoneyTransactionFilterTest) -// using namespace std; +MyMoneyTransactionFilterTest::MyMoneyTransactionFilterTest::MyMoneyTransactionFilterTest() + : storage(nullptr) + , file(nullptr) +{ +} void MyMoneyTransactionFilterTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest("Payee 10.2"); file->addPayee(payeeTest); payeeId = payeeTest.id(); MyMoneyTag tag("Tag 10.2"); file->addTag(tag); tagIdList << tag.id(); QString acAsset = MyMoneyFile::instance()->asset().id(); QString acExpense = (MyMoneyFile::instance()->expense().id()); QString acIncome = (MyMoneyFile::instance()->income().id()); acCheckingId = makeAccount("Account 10.2", eMyMoney::Account::Type::Checkings, MyMoneyMoney(0.0), QDate(2004, 1, 1), acAsset); acExpenseId = makeAccount("Expense", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acIncomeId = makeAccount("Expense", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acIncome); ft.commit(); } void MyMoneyTransactionFilterTest::cleanup() { file->detachStorage(storage); delete storage; } void MyMoneyTransactionFilterTest::testMatchAmount() { MyMoneySplit split; split.setShares(MyMoneyMoney(123.20)); MyMoneyTransactionFilter filter; QCOMPARE(filter.matchAmount(split), true); filter.setAmountFilter(MyMoneyMoney("123.0"), MyMoneyMoney("124.0")); QCOMPARE(filter.matchAmount(split), true); filter.setAmountFilter(MyMoneyMoney("120.0"), MyMoneyMoney("123.0")); QCOMPARE(filter.matchAmount(split), false); } void MyMoneyTransactionFilterTest::testMatchText() { MyMoneySplit split; MyMoneyTransactionFilter filter; MyMoneyAccount account = file->account(acCheckingId); // no filter QCOMPARE(filter.matchText(split, account), true); filter.setTextFilter(QRegExp("10.2"), false); MyMoneyTransactionFilter filterInvert; filterInvert.setTextFilter(QRegExp("10.2"), true); MyMoneyTransactionFilter filterNotFound; filterNotFound.setTextFilter(QRegExp("10.5"), false); // memo split.setMemo("10.2"); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setMemo(QString()); // payee split.setPayeeId(payeeId); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setPayeeId(QString()); // tag split.setTagIdList(tagIdList); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setTagIdList(QStringList()); // value split.setValue(MyMoneyMoney("10.2")); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setValue(MyMoneyMoney()); // number split.setNumber("10.2"); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setNumber("0.0"); // transaction id split.setTransactionId("10.2"); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setTransactionId("0.0"); // account split.setAccountId(acCheckingId); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); } void MyMoneyTransactionFilterTest::testMatchSplit() { qDebug() << "returns matchText() || matchAmount(), which are already tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionAll() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); } void MyMoneyTransactionFilterTest::testMatchTransactionAccount() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.addAccount(acCheckingId); filter.setReportAllSplits(true); filter.setConsiderCategory(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(false); filter.setConsiderCategory(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(false); filter.setConsiderCategory(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(true); filter.setConsiderCategory(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); } void MyMoneyTransactionFilterTest::testMatchTransactionCategory() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.addCategory(acExpenseId); filter.setReportAllSplits(true); filter.setConsiderCategory(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setConsiderCategory(false); QVERIFY(!filter.match(transaction)); } void MyMoneyTransactionFilterTest::testMatchTransactionDate() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); filter.setDateFilter(QDate(2014, 1, 1), QDate(2014, 1, 3)); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setDateFilter(QDate(2014, 1, 3), QDate(2014, 1, 5)); QVERIFY(!filter.match(transaction)); } void setupTransactionForNumber(MyMoneyTransaction &transaction, const QString &accountId) { MyMoneySplit split; split.setAccountId(accountId); split.setShares(MyMoneyMoney(123.00)); split.setNumber("1"); split.setMemo("1"); MyMoneySplit split2; split2.setAccountId(accountId); split2.setShares(MyMoneyMoney(1.00)); split2.setNumber("2"); split2.setMemo("2"); MyMoneySplit split3; split3.setAccountId(accountId); split3.setShares(MyMoneyMoney(100.00)); split3.setNumber("3"); split3.setMemo("3"); MyMoneySplit split4; split4.setAccountId(accountId); split4.setShares(MyMoneyMoney(22.00)); split4.setNumber("4"); split4.setMemo("4"); transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); transaction.addSplit(split3); transaction.addSplit(split4); } void runtTestMatchTransactionNumber(MyMoneyTransaction &transaction, MyMoneyTransactionFilter &filter) { // return all matching splits filter.setReportAllSplits(true); filter.setNumberFilter("", ""); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("1", ""); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("", "4"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("1", "4"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("1", "2"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); // do not return all matching splits filter.setReportAllSplits(false); filter.setNumberFilter("1", "4"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setNumberFilter("1", "2"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); } void MyMoneyTransactionFilterTest::testMatchTransactionNumber() { MyMoneyTransaction transaction; setupTransactionForNumber(transaction, acCheckingId); MyMoneyTransactionFilter filter; runtTestMatchTransactionNumber(transaction, filter); transaction.clear(); setupTransactionForNumber(transaction, acExpenseId); filter.clear(); runtTestMatchTransactionNumber(transaction, filter); } void MyMoneyTransactionFilterTest::testMatchTransactionPayee() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); split.setPayeeId(payeeId); MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setShares(MyMoneyMoney(124.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.addPayee(payeeId); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check no category support MyMoneySplit split3; split3.setAccountId(acExpenseId); split3.setShares(MyMoneyMoney(120.00)); split3.setPayeeId(payeeId); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split3); filter.setReportAllSplits(true); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); qDebug() << "payee on categories could not be tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionState() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setShares(MyMoneyMoney(1.00)); split2.setReconcileFlag(eMyMoney::Split::State::Cleared); MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setShares(MyMoneyMoney(100.00)); split3.setReconcileFlag(eMyMoney::Split::State::Reconciled); MyMoneySplit split4; split4.setAccountId(acCheckingId); split4.setShares(MyMoneyMoney(22.00)); split4.setReconcileFlag(eMyMoney::Split::State::Frozen); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); transaction.addSplit(split3); transaction.addSplit(split4); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); // all states filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::Reconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Frozen); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); // single state filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::Reconciled); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::Frozen); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check no category support MyMoneySplit split5; split5.setAccountId(acCheckingId); split5.setShares(MyMoneyMoney(22.00)); split5.setReconcileFlag(eMyMoney::Split::State::Frozen); MyMoneyTransaction transaction2; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split5); filter.clear(); filter.setReportAllSplits(true); filter.addState((int)eMyMoney::TransactionFilter::State::Frozen); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); qDebug() << "states on categories could not be tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionTag() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); split.setTagIdList(tagIdList); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); split2.setTagIdList(tagIdList); MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setShares(MyMoneyMoney(10.00)); split3.setTagIdList(tagIdList); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); transaction.addSplit(split3); MyMoneyTransactionFilter filter; filter.addTag(tagIdList.first()); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); // -1 because categories are not supported yet QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check no category support MyMoneySplit split4; split4.setAccountId(acExpenseId); split4.setShares(MyMoneyMoney(123.00)); split4.setTagIdList(tagIdList); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split4); filter.setReportAllSplits(true); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); qDebug() << "tags on categories could not be tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionTypeAllTypes() { /* alltypes - account group == MyMoneyAccount::Income || - account group == MyMoneyAccount::Expense */ MyMoneySplit split; split.setAccountId(acExpenseId); split.setValue(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acIncomeId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.addType((int)eMyMoney::TransactionFilter::State::All); qDebug() << "MyMoneyTransactionFilter::allTypes could not be tested"; qDebug() << "because type filter does not work with categories"; QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // ! alltypes MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction2; transaction2.addSplit(split3); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); } void MyMoneyTransactionFilterTest::testMatchTransactionTypeDeposits() { // deposits - split value is positive MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // deposits filter.addType((int)eMyMoney::TransactionFilter::Type::Deposits); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // no deposits MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split2); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); } void MyMoneyTransactionFilterTest::testMatchTransactionTypePayments() { /* payments - account group != MyMoneyAccount::Income - account group != MyMoneyAccount::Expense - split value is not positive - number of splits != 2 */ MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // valid payments filter.addType((int)eMyMoney::TransactionFilter::Type::Payments); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // no payments // check number of splits != 2 MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setValue(MyMoneyMoney(-123.00)); transaction.addSplit(split2); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // split value is not positive MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setValue(MyMoneyMoney(123.00)); transaction.addSplit(split3); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); // account group != MyMoneyAccount::Income && account group != MyMoneyAccount::Expense MyMoneySplit split4; split4.setAccountId(acExpenseId); split4.setValue(MyMoneyMoney(-124.00)); transaction.addSplit(split4); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); } void MyMoneyTransactionFilterTest::testMatchTransactionTypeTransfers() { /* check transfers - number of splits == 2 - account group != MyMoneyAccount::Income - account group != MyMoneyAccount::Expense */ MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.addType((int)eMyMoney::TransactionFilter::Type::Transfers); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); // transfers - invalid number of counts transaction.addSplit(split3); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // transfers - invalid account MyMoneySplit split4; split4.setAccountId(acIncomeId); split4.setValue(MyMoneyMoney(-123.00)); MyMoneySplit split5; split5.setAccountId(acCheckingId); split5.setValue(MyMoneyMoney(123.00)); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split4); transaction2.addSplit(split5); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); } void MyMoneyTransactionFilterTest::testMatchTransactionValidity() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); // check valid transaction MyMoneyTransactionFilter filter; filter.addValidity((int)eMyMoney::TransactionFilter::Validity::Valid); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check invalid transaction filter.clear(); filter.addValidity((int)eMyMoney::TransactionFilter::Validity::Invalid); filter.setReportAllSplits(true); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // add split to make transaction invalid MyMoneySplit split3; split3.setAccountId(acExpenseId); split3.setValue(MyMoneyMoney(-10.00)); transaction.addSplit(split3); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 3); filter.clear(); filter.addValidity((int)eMyMoney::TransactionFilter::Validity::Valid); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); } diff --git a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h index 96c3e87cf..aaaed562f 100644 --- a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h +++ b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h @@ -1,58 +1,61 @@ /* * Copyright 2018 Ralf Habacker * Copyright 2018 Thomas Baumgart * * 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 MYMONEYTRANSACTIONFILTERTEST_H #define MYMONEYTRANSACTIONFILTERTEST_H #include class MyMoneyStorageMgr; class MyMoneyFile; class MyMoneyTransactionFilterTest : public QObject { - Q_OBJECT + Q_OBJECT +public: + MyMoneyTransactionFilterTest(); + private slots: void init(); void cleanup(); void testMatchAmount(); void testMatchText(); void testMatchSplit(); void testMatchTransactionAll(); void testMatchTransactionAccount(); void testMatchTransactionCategory(); void testMatchTransactionDate(); void testMatchTransactionNumber(); void testMatchTransactionPayee(); void testMatchTransactionState(); void testMatchTransactionTag(); void testMatchTransactionTypeAllTypes(); void testMatchTransactionTypeDeposits(); void testMatchTransactionTypePayments(); void testMatchTransactionTypeTransfers(); void testMatchTransactionValidity(); private: QString payeeId; QList tagIdList; QString acCheckingId; QString acExpenseId; QString acIncomeId; MyMoneyStorageMgr* storage; MyMoneyFile* file; }; #endif diff --git a/kmymoney/plugins/kbanking/kbanking.cpp b/kmymoney/plugins/kbanking/kbanking.cpp index c67d24563..0fedfb399 100644 --- a/kmymoney/plugins/kbanking/kbanking.cpp +++ b/kmymoney/plugins/kbanking/kbanking.cpp @@ -1,1529 +1,1530 @@ /* * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * Copyright 2009 Cristian Onet onet.cristian@gmail.com * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org * Copyright 2015 Christian David christian-david@web.de * * 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 #include "kbanking.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include //! @todo remove @c #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Library Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/onlinejob.h" #include "kbaccountsettings.h" #include "kbmapaccount.h" #include "mymoneyfile.h" #include "onlinejobadministration.h" #include "kmymoneyview.h" #include "kbpickstartdate.h" #include "mymoneyinstitution.h" #include "mymoneyexception.h" #include "gwenkdegui.h" #include "gwenhywfarqtoperators.h" #include "aqbankingkmmoperators.h" #include "mymoneystatement.h" #include "statementinterface.h" #include "viewinterface.h" #ifdef KMM_DEBUG // Added an option to open the chipTanDialog from the menu for debugging purposes #include "chiptandialog.h" #endif class KBanking::Private { public: Private() : passwordCacheTimer(nullptr), jobList(), fileId() { QString gwenProxy = QString::fromLocal8Bit(qgetenv("GWEN_PROXY")); if (gwenProxy.isEmpty()) { std::unique_ptr cfg = std::unique_ptr(new KConfig("kioslaverc")); QRegExp exp("(\\w+://)?([^/]{2}.+:\\d+)"); QString proxy; KConfigGroup grp = cfg->group("Proxy Settings"); int type = grp.readEntry("ProxyType", 0); switch (type) { case 0: // no proxy break; case 1: // manual specified proxy = grp.readEntry("httpsProxy"); qDebug("KDE https proxy setting is '%s'", qPrintable(proxy)); if (exp.exactMatch(proxy)) { proxy = exp.cap(2); qDebug("Setting GWEN_PROXY to '%s'", qPrintable(proxy)); if (!qputenv("GWEN_PROXY", qPrintable(proxy))) { qDebug("Unable to setup GWEN_PROXY"); } } break; default: // other currently not supported qDebug("KDE proxy setting of type %d not supported", type); break; } } } /** * KMyMoney asks for accounts over and over again which causes a lot of "Job not supported with this account" error messages. * This function filters messages with that string. */ static int gwenLogHook(GWEN_GUI* gui, const char* domain, GWEN_LOGGER_LEVEL level, const char* message) { Q_UNUSED(gui); Q_UNUSED(domain); Q_UNUSED(level); const char* messageToFilter = "Job not supported with this account"; if (strstr(message, messageToFilter) != 0) return 1; return 0; } QTimer *passwordCacheTimer; QMap jobList; QString fileId; }; KBanking::KBanking(QObject *parent, const QVariantList &args) : OnlinePluginExtended(parent, "kbanking") , d(new Private) , m_configAction(nullptr) , m_importAction(nullptr) , m_kbanking(nullptr) , m_accountSettings(nullptr) + , m_statementCount(0) { Q_UNUSED(args) qDebug("Plugins: kbanking loaded"); } KBanking::~KBanking() { delete d; qDebug("Plugins: kbanking unloaded"); } void KBanking::plug() { m_kbanking = new KBankingExt(this, "KMyMoney"); d->passwordCacheTimer = new QTimer(this); d->passwordCacheTimer->setSingleShot(true); d->passwordCacheTimer->setInterval(60000); connect(d->passwordCacheTimer, &QTimer::timeout, this, &KBanking::slotClearPasswordCache); if (m_kbanking) { if (AB_Banking_HasConf4(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB4 config found."); if (AB_Banking_HasConf3(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB3 config found."); if (!AB_Banking_HasConf2(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: AqB2 config found - converting."); AB_Banking_ImportConf2(m_kbanking->getCInterface()); } } else { qDebug("KBankingPlugin: AqB3 config found - converting."); AB_Banking_ImportConf3(m_kbanking->getCInterface()); } } //! @todo when is gwenKdeGui deleted? gwenKdeGui *gui = new gwenKdeGui(); GWEN_Gui_SetGui(gui->getCInterface()); GWEN_Logger_SetLevel(0, GWEN_LoggerLevel_Warning); if (m_kbanking->init() == 0) { // Tell the host application to load my GUI component setComponentName("kbanking", "KBanking"); setXMLFile("kbanking.rc"); // get certificate handling and dialog settings management AB_Gui_Extend(gui->getCInterface(), m_kbanking->getCInterface()); // create actions createActions(); // load protocol conversion list loadProtocolConversion(); GWEN_Logger_SetLevel(AQBANKING_LOGDOMAIN, GWEN_LoggerLevel_Warning); GWEN_Gui_SetLogHookFn(GWEN_Gui_GetGui(), &KBanking::Private::gwenLogHook); } else { qWarning("Could not initialize KBanking online banking interface"); delete m_kbanking; m_kbanking = 0; } } } void KBanking::unplug() { d->passwordCacheTimer->deleteLater(); if (m_kbanking) { m_kbanking->fini(); delete m_kbanking; qDebug("Plugins: kbanking unpluged"); } } void KBanking::loadProtocolConversion() { if (m_kbanking) { m_protocolConversionMap = { {"aqhbci", "HBCI"}, {"aqofxconnect", "OFX"}, {"aqyellownet", "YellowNet"}, {"aqgeldkarte", "Geldkarte"}, {"aqdtaus", "DTAUS"} }; } } void KBanking::protocols(QStringList& protocolList) const { if (m_kbanking) { std::list list = m_kbanking->getActiveProviders(); std::list::iterator it; for (it = list.begin(); it != list.end(); ++it) { // skip the dummy if (*it == "aqnone") continue; QMap::const_iterator it_m; it_m = m_protocolConversionMap.find((*it).c_str()); if (it_m != m_protocolConversionMap.end()) protocolList << (*it_m); else protocolList << (*it).c_str(); } } } QWidget* KBanking::accountConfigTab(const MyMoneyAccount& acc, QString& name) { const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); name = i18n("Online settings"); if (m_kbanking) { m_accountSettings = new KBAccountSettings(acc, 0); m_accountSettings->loadUi(kvp); return m_accountSettings; } QLabel* label = new QLabel(i18n("KBanking module not correctly initialized"), 0); label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); return label; } MyMoneyKeyValueContainer KBanking::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); kvp["provider"] = objectName().toLower(); if (m_accountSettings) { m_accountSettings->loadKvp(kvp); } return kvp; } void KBanking::createActions() { QAction *settings_aqbanking = actionCollection()->addAction("settings_aqbanking"); settings_aqbanking->setText(i18n("Configure Aq&Banking...")); connect(settings_aqbanking, &QAction::triggered, this, &KBanking::slotSettings); QAction *file_import_aqbanking = actionCollection()->addAction("file_import_aqbanking"); file_import_aqbanking->setText(i18n("AqBanking importer...")); connect(file_import_aqbanking, &QAction::triggered, this, &KBanking::slotImport); Q_CHECK_PTR(viewInterface()); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action("file_import_aqbanking"), &QAction::setEnabled); #ifdef KMM_DEBUG QAction *openChipTanDialog = actionCollection()->addAction("open_chiptan_dialog"); openChipTanDialog->setText("Open ChipTan Dialog"); connect(openChipTanDialog, &QAction::triggered, [&](){ auto dlg = new chipTanDialog(); dlg->setHhdCode("0F04871100030333555414312C32331D"); dlg->setInfoText("

Test Graphic for debugging

The encoded data is

Account Number: 335554
Amount: 1,23

"); connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater); connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater); dlg->show(); }); #endif } void KBanking::slotSettings() { if (m_kbanking) { GWEN_DIALOG* dlg = AB_SetupDialog_new(m_kbanking->getCInterface()); if (dlg == NULL) { DBG_ERROR(0, "Could not create setup dialog."); return; } if (GWEN_Gui_ExecDialog(dlg, 0) == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); return; } GWEN_Dialog_free(dlg); } } bool KBanking::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { bool rc = false; if (m_kbanking && !acc.id().isEmpty()) { m_kbanking->askMapAccount(acc); // at this point, the account should be mapped // so we search it and setup the account reference in the KMyMoney object AB_ACCOUNT* ab_acc; ab_acc = aqbAccount(acc); if (ab_acc) { MyMoneyAccount a(acc); setupAccountReference(a, ab_acc); settings = a.onlineBankingSettings(); rc = true; } } return rc; } AB_ACCOUNT* KBanking::aqbAccount(const MyMoneyAccount& acc) const { if (m_kbanking == 0) { return 0; } // certainly looking for an expense or income account does not make sense at this point // so we better get out right away if (acc.isIncomeExpense()) { return 0; } AB_ACCOUNT *ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), m_kbanking->mappingId(acc).toUtf8().data()); // if the account is not found, we temporarily scan for the 'old' mapping (the one w/o the file id) // in case we find it, we setup the new mapping in addition on the fly. if (!ab_acc && acc.isAssetLiability()) { ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), acc.id().toUtf8().data()); if (ab_acc) { qDebug("Found old mapping for '%s' but not new. Setup new mapping", qPrintable(acc.name())); m_kbanking->setAccountAlias(ab_acc, m_kbanking->mappingId(acc).toUtf8().constData()); // TODO at some point in time, we should remove the old mapping } } return ab_acc; } AB_ACCOUNT* KBanking::aqbAccount(const QString& accountId) const { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); return aqbAccount(account); } QString KBanking::stripLeadingZeroes(const QString& s) const { QString rc(s); QRegExp exp("^(0*)([^0].*)"); if (exp.exactMatch(s)) { rc = exp.cap(2); } return rc; } void KBanking::setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc) { MyMoneyKeyValueContainer kvp; if (ab_acc) { QString accountNumber = stripLeadingZeroes(AB_Account_GetAccountNumber(ab_acc)); QString routingNumber = stripLeadingZeroes(AB_Account_GetBankCode(ab_acc)); QString val = QString("%1-%2").arg(routingNumber, accountNumber); if (val != acc.onlineBankingSettings().value("kbanking-acc-ref")) { kvp.clear(); // make sure to keep our own previous settings const QMap& vals = acc.onlineBankingSettings().pairs(); QMap::const_iterator it_p; for (it_p = vals.begin(); it_p != vals.end(); ++it_p) { if (QString(it_p.key()).startsWith("kbanking-")) { kvp.setValue(it_p.key(), *it_p); } } kvp.setValue("kbanking-acc-ref", val); kvp.setValue("provider", objectName().toLower()); setAccountOnlineParameters(acc, kvp); } } else { // clear the connection setAccountOnlineParameters(acc, kvp); } } bool KBanking::accountIsMapped(const MyMoneyAccount& acc) { return aqbAccount(acc) != 0; } bool KBanking::updateAccount(const MyMoneyAccount& acc) { return updateAccount(acc, false); } bool KBanking::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { if (!m_kbanking) return false; bool rc = false; if (!acc.id().isEmpty()) { AB_JOB *job = 0; int rv; /* get AqBanking account */ AB_ACCOUNT *ba = aqbAccount(acc); // Update the connection between the KMyMoney account and the AqBanking equivalent. // If the account is not found anymore ba == 0 and the connection is removed. setupAccountReference(acc, ba); if (!ba) { KMessageBox::error(0, i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", acc.name()), i18n("Account Not Mapped")); } else { bool enqueJob = true; if (acc.onlineBankingSettings().value("kbanking-txn-download") != "no") { /* create getTransactions job */ job = AB_JobGetTransactions_new(ba); rv = AB_Job_CheckAvailability(job); if (rv) { DBG_ERROR(0, "Job \"GetTransactions\" is not available (%d)", rv); KMessageBox::error(0, i18n("" "The update job is not supported by the " "bank/account/backend.\n" ""), i18n("Job not Available")); AB_Job_free(job); job = 0; } if (job) { int days = AB_JobGetTransactions_GetMaxStoreDays(job); QDate qd; if (days > 0) { GWEN_TIME *ti1; GWEN_TIME *ti2; ti1 = GWEN_CurrentTime(); ti2 = GWEN_Time_fromSeconds(GWEN_Time_Seconds(ti1) - (60 * 60 * 24 * days)); GWEN_Time_free(ti1); ti1 = ti2; int year, month, day; if (GWEN_Time_GetBrokenDownDate(ti1, &day, &month, &year)) { DBG_ERROR(0, "Bad date"); qd = QDate(); } else qd = QDate(year, month + 1, day); GWEN_Time_free(ti1); } // get last statement request date from application account object // and start from a few days before if the date is valid QDate lastUpdate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate); if (lastUpdate.isValid()) lastUpdate = lastUpdate.addDays(-3); int dateOption = acc.onlineBankingSettings().value("kbanking-statementDate").toInt(); switch (dateOption) { case 0: // Ask user break; case 1: // No date qd = QDate(); break; case 2: // Last download qd = lastUpdate; break; case 3: // First possible // qd is already setup break; } // the pick start date option dialog is needed in // case the dateOption is 0 or the date option is > 1 // and the qd is invalid if (dateOption == 0 || (dateOption > 1 && !qd.isValid())) { QPointer psd = new KBPickStartDate(m_kbanking, qd, lastUpdate, acc.name(), lastUpdate.isValid() ? 2 : 3, 0, true); if (psd->exec() == QDialog::Accepted) { qd = psd->date(); } else { enqueJob = false; } delete psd; } if (enqueJob) { if (qd.isValid()) { GWEN_TIME *ti1; ti1 = GWEN_Time_new(qd.year(), qd.month() - 1, qd.day(), 0, 0, 0, 0); AB_JobGetTransactions_SetFromTime(job, ti1); GWEN_Time_free(ti1); } rv = m_kbanking->enqueueJob(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } } AB_Job_free(job); } } if (enqueJob) { /* create getBalance job */ job = AB_JobGetBalance_new(ba); rv = AB_Job_CheckAvailability(job); if (!rv) rv = m_kbanking->enqueueJob(job); else rv = 0; AB_Job_free(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } else { rc = true; emit queueChanged(); } } } } // make sure we have at least one job in the queue before sending it if (!moreAccounts && m_kbanking->getEnqueuedJobs().size() > 0) executeQueue(); return rc; } void KBanking::executeQueue() { if (m_kbanking && m_kbanking->getEnqueuedJobs().size() > 0) { AB_IMEXPORTER_CONTEXT *ctx; ctx = AB_ImExporterContext_new(); int rv = m_kbanking->executeQueue(ctx); if (!rv) { m_kbanking->importContext(ctx, 0); } else { DBG_ERROR(0, "Error: %d", rv); } AB_ImExporterContext_free(ctx); } } /** @todo improve error handling, e.g. by adding a .isValid to nationalTransfer * @todo use new onlineJob system */ void KBanking::sendOnlineJob(QList& jobs) { Q_CHECK_PTR(m_kbanking); m_onlineJobQueue.clear(); QList unhandledJobs; if (!jobs.isEmpty()) { foreach (onlineJob job, jobs) { if (sepaOnlineTransfer::name() == job.task()->taskName()) { onlineJobTyped typedJob(job); enqueTransaction(typedJob); job = typedJob; } else { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "KBanking", "Cannot handle this request")); unhandledJobs.append(job); } m_onlineJobQueue.insert(m_kbanking->mappingId(job), job); } executeQueue(); } jobs = m_onlineJobQueue.values() + unhandledJobs; m_onlineJobQueue.clear(); } QStringList KBanking::availableJobs(QString accountId) { try { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString id = MyMoneyFile::instance()->value("kmm-id"); if(id != d->fileId) { d->jobList.clear(); d->fileId = id; } } catch (const MyMoneyException &) { // Exception usually means account was not found return QStringList(); } if(d->jobList.contains(accountId)) { return d->jobList[accountId]; } QStringList list; AB_ACCOUNT* abAccount = aqbAccount(accountId); if (!abAccount) { return list; } // Check availableJobs // sepa transfer AB_JOB* abJob = AB_JobSepaTransfer_new(abAccount); if (AB_Job_CheckAvailability(abJob) == 0) list.append(sepaOnlineTransfer::name()); AB_Job_free(abJob); d->jobList[accountId] = list; return list; } /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbJobDeleter { public: static void cleanup(AB_JOB* job) { AB_Job_free(job); } }; /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbAccountDeleter { public: static void cleanup(AB_ACCOUNT* account) { AB_Account_free(account); } }; IonlineTaskSettings::ptr KBanking::settings(QString accountId, QString taskName) { AB_ACCOUNT* abAcc = aqbAccount(accountId); if (abAcc == 0) return IonlineTaskSettings::ptr(); if (sepaOnlineTransfer::name() == taskName) { // Get limits for sepaonlinetransfer QScopedPointer abJob(AB_JobSepaTransfer_new(abAcc)); if (AB_Job_CheckAvailability(abJob.data()) != 0) return IonlineTaskSettings::ptr(); const AB_TRANSACTION_LIMITS* limits = AB_Job_GetFieldLimits(abJob.data()); return AB_TransactionLimits_toSepaOnlineTaskSettings(limits).dynamicCast(); } return IonlineTaskSettings::ptr(); } bool KBanking::enqueTransaction(onlineJobTyped& job) { /* get AqBanking account */ const QString accId = job.constTask()->responsibleAccount(); AB_ACCOUNT *abAccount = aqbAccount(accId); if (!abAccount) { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Warning, "KBanking", i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", MyMoneyFile::instance()->account(accId).name()))); return false; } //setupAccountReference(acc, ba); // needed? AB_JOB *abJob = AB_JobSepaTransfer_new(abAccount); int rv = AB_Job_CheckAvailability(abJob); if (rv) { qDebug("AB_ERROR_OFFSET is %i", AB_ERROR_OFFSET); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "AqBanking", QString("Sepa credit transfers for account \"%1\" are not available, error code %2.").arg(MyMoneyFile::instance()->account(accId).name(), rv) ) ); return false; } AB_TRANSACTION *AbTransaction = AB_Transaction_new(); // Recipient payeeIdentifiers::ibanBic beneficiaryAcc = job.constTask()->beneficiaryTyped(); AB_Transaction_SetRemoteName(AbTransaction, GWEN_StringList_fromQString(beneficiaryAcc.ownerName())); AB_Transaction_SetRemoteIban(AbTransaction, beneficiaryAcc.electronicIban().toUtf8().constData()); AB_Transaction_SetRemoteBic(AbTransaction, beneficiaryAcc.fullStoredBic().toUtf8().constData()); // Origin Account AB_Transaction_SetLocalAccount(AbTransaction, abAccount); // Purpose QStringList qPurpose = job.constTask()->purpose().split('\n'); GWEN_STRINGLIST *purpose = GWEN_StringList_fromQStringList(qPurpose); AB_Transaction_SetPurpose(AbTransaction, purpose); GWEN_StringList_free(purpose); // Reference // AqBanking duplicates the string. This should be safe. AB_Transaction_SetEndToEndReference(AbTransaction, job.constTask()->endToEndReference().toUtf8().constData()); // Other Fields AB_Transaction_SetTextKey(AbTransaction, job.constTask()->textKey()); AB_Transaction_SetValue(AbTransaction, AB_Value_fromMyMoneyMoney(job.constTask()->value())); /** @todo LOW remove Debug info */ qDebug() << "SetTransaction: " << AB_Job_SetTransaction(abJob, AbTransaction); GWEN_DB_NODE *gwenNode = AB_Job_GetAppData(abJob); GWEN_DB_SetCharValue(gwenNode, GWEN_DB_FLAGS_DEFAULT, "kmmOnlineJobId", m_kbanking->mappingId(job).toLatin1().constData()); qDebug() << "Enqueue: " << m_kbanking->enqueueJob(abJob); //delete localAcc; return true; } void KBanking::startPasswordTimer() { if (d->passwordCacheTimer->isActive()) d->passwordCacheTimer->stop(); d->passwordCacheTimer->start(); } void KBanking::slotClearPasswordCache() { m_kbanking->clearPasswordCache(); } void KBanking::slotImport() { m_statementCount = 0; statementInterface()->resetMessages(); if (!m_kbanking->interactiveImport()) qWarning("Error on import dialog"); else statementInterface()->showMessages(m_statementCount); } bool KBanking::importStatement(const MyMoneyStatement& s) { m_statementCount++; return !statementInterface()->import(s).isEmpty(); } MyMoneyAccount KBanking::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void KBanking::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const { return statementInterface()->setAccountOnlineParameters(acc, kvps); } KBankingExt::KBankingExt(KBanking* parent, const char* appname, const char* fname) : AB_Banking(appname, fname) , m_parent(parent) , _jobQueue(0) { m_sepaKeywords = {QString::fromUtf8("SEPA-BASISLASTSCHRIFT"), QString::fromUtf8("SEPA-ÜBERWEISUNG")}; } int KBankingExt::init() { int rv = AB_Banking::init(); if (rv < 0) return rv; rv = onlineInit(); if (rv) { fprintf(stderr, "Error on online init (%d).\n", rv); AB_Banking::fini(); return rv; } _jobQueue = AB_Job_List2_new(); return 0; } int KBankingExt::fini() { if (_jobQueue) { AB_Job_List2_FreeAll(_jobQueue); _jobQueue = 0; } const int rv = onlineFini(); if (rv) { AB_Banking::fini(); return rv; } return AB_Banking::fini(); } int KBankingExt::executeQueue(AB_IMEXPORTER_CONTEXT *ctx) { m_parent->startPasswordTimer(); int rv = AB_Banking::executeJobs(_jobQueue, ctx); if (rv != 0) { qDebug() << "Sending queue by aqbanking got error no " << rv; } /** check result of each job */ AB_JOB_LIST2_ITERATOR* jobIter = AB_Job_List2_First(_jobQueue); if (jobIter) { AB_JOB* abJob = AB_Job_List2Iterator_Data(jobIter); while (abJob) { GWEN_DB_NODE* gwenNode = AB_Job_GetAppData(abJob); if (gwenNode == 0) { qWarning("Executed AB_Job without KMyMoney id"); abJob = AB_Job_List2Iterator_Next(jobIter); break; } QString jobIdent = QString::fromUtf8(GWEN_DB_GetCharValue(gwenNode, "kmmOnlineJobId", 0, "")); onlineJob job = m_parent->m_onlineJobQueue.value(jobIdent); if (job.isNull()) { // It should not be possiblie that this will happen (only if AqBanking fails heavily). //! @todo correct exception text qWarning("Executed a job which was not in queue. Please inform the KMyMoney developers."); abJob = AB_Job_List2Iterator_Next(jobIter); continue; } AB_JOB_STATUS abStatus = AB_Job_GetStatus(abJob); if (abStatus == AB_Job_StatusSent || abStatus == AB_Job_StatusPending || abStatus == AB_Job_StatusFinished || abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setJobSend(); if (abStatus == AB_Job_StatusFinished) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::acceptedByBank); else if (abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::sendingError); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KBanking", "Job was processed")); m_parent->m_onlineJobQueue.insert(jobIdent, job); abJob = AB_Job_List2Iterator_Next(jobIter); } AB_Job_List2Iterator_free(jobIter); } AB_JOB_LIST2 *oldQ = _jobQueue; _jobQueue = AB_Job_List2_new(); AB_Job_List2_FreeAll(oldQ); emit m_parent->queueChanged(); m_parent->startPasswordTimer(); return rv; } void KBankingExt::clearPasswordCache() { /* clear password DB */ GWEN_Gui_SetPasswordStatus(NULL, NULL, GWEN_Gui_PasswordStatus_Remove, 0); } std::list KBankingExt::getEnqueuedJobs() { AB_JOB_LIST2 *ll; std::list rl; ll = _jobQueue; if (ll && AB_Job_List2_GetSize(ll)) { AB_JOB *j; AB_JOB_LIST2_ITERATOR *it; it = AB_Job_List2_First(ll); assert(it); j = AB_Job_List2Iterator_Data(it); assert(j); while (j) { rl.push_back(j); j = AB_Job_List2Iterator_Next(it); } AB_Job_List2Iterator_free(it); } return rl; } int KBankingExt::enqueueJob(AB_JOB *j) { assert(_jobQueue); assert(j); AB_Job_Attach(j); AB_Job_List2_PushBack(_jobQueue, j); return 0; } int KBankingExt::dequeueJob(AB_JOB *j) { assert(_jobQueue); AB_Job_List2_Remove(_jobQueue, j); AB_Job_free(j); emit m_parent->queueChanged(); return 0; } void KBankingExt::transfer() { //m_parent->transfer(); } bool KBankingExt::askMapAccount(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); QString bankId; QString accountId; // extract some information about the bank. if we have a sortcode // (BLZ) we display it, otherwise the name is enough. try { const MyMoneyInstitution &bank = file->institution(acc.institutionId()); bankId = bank.name(); if (!bank.sortcode().isEmpty()) bankId = bank.sortcode(); } catch (const MyMoneyException &e) { // no bank assigned, we just leave the field emtpy } // extract account information. if we have an account number // we show it, otherwise the name will be displayed accountId = acc.number(); if (accountId.isEmpty()) accountId = acc.name(); // do the mapping. the return value of this method is either // true, when the user mapped the account or false, if he // decided to quit the dialog. So not really a great thing // to present some more information. KBMapAccount *w; w = new KBMapAccount(this, bankId.toUtf8().constData(), accountId.toUtf8().constData()); if (w->exec() == QDialog::Accepted) { AB_ACCOUNT *a; a = w->getAccount(); assert(a); DBG_NOTICE(0, "Mapping application account \"%s\" to " "online account \"%s/%s\"", qPrintable(acc.name()), AB_Account_GetBankCode(a), AB_Account_GetAccountNumber(a)); // TODO remove the following line once we don't need backward compatibility setAccountAlias(a, acc.id().toUtf8().constData()); qDebug("Setup mapping to '%s'", acc.id().toUtf8().constData()); setAccountAlias(a, mappingId(acc).toUtf8().constData()); qDebug("Setup mapping to '%s'", mappingId(acc).toUtf8().constData()); delete w; return true; } delete w; return false; } QString KBankingExt::mappingId(const MyMoneyObject& object) const { QString id = MyMoneyFile::instance()->storageId() + QLatin1Char('-') + object.id(); // AqBanking does not handle the enclosing parens, so we remove it id.remove('{'); id.remove('}'); return id; } bool KBankingExt::interactiveImport() { AB_IMEXPORTER_CONTEXT *ctx; GWEN_DIALOG *dlg; int rv; ctx = AB_ImExporterContext_new(); dlg = AB_ImporterDialog_new(getCInterface(), ctx, NULL); if (dlg == NULL) { DBG_ERROR(0, "Could not create importer dialog."); AB_ImExporterContext_free(ctx); return false; } rv = GWEN_Gui_ExecDialog(dlg, 0); if (rv == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } if (!importContext(ctx, 0)) { DBG_ERROR(0, "Error on importContext"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return true; } const AB_ACCOUNT_STATUS* KBankingExt::_getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai) { const AB_ACCOUNT_STATUS *ast; const AB_ACCOUNT_STATUS *best; best = 0; ast = AB_ImExporterAccountInfo_GetFirstAccountStatus(ai); while (ast) { if (!best) best = ast; else { const GWEN_TIME *tiBest; const GWEN_TIME *ti; tiBest = AB_AccountStatus_GetTime(best); ti = AB_AccountStatus_GetTime(ast); if (!tiBest) { best = ast; } else { if (ti) { double d; /* we have two times, compare them */ d = GWEN_Time_Diff(ti, tiBest); if (d > 0) /* newer */ best = ast; } } } ast = AB_ImExporterAccountInfo_GetNextAccountStatus(ai); } /* while */ return best; } void KBankingExt::_xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount& acc, const AB_TRANSACTION *t) { const GWEN_STRINGLIST *sl; QString s; QString memo; const char *p; const AB_VALUE *val; const GWEN_TIME *ti; const GWEN_TIME *startTime = 0; MyMoneyStatement::Transaction kt; unsigned long h; kt.m_fees = MyMoneyMoney(); // bank's transaction id p = AB_Transaction_GetFiId(t); if (p) kt.m_strBankID = QString("ID ") + QString::fromUtf8(p); // payee s.truncate(0); sl = AB_Transaction_GetRemoteName(t); if (sl) { GWEN_STRINGLISTENTRY *se; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); s += QString::fromUtf8(p); se = GWEN_StringListEntry_Next(se); } // while } kt.m_strPayee = s; // memo // The variable 's' contains the old method of extracting // the memo which added a linefeed after each part received // from AqBanking. The new variable 'memo' does not have // this inserted linefeed. We keep the variable 's' to // construct the hash-value to retrieve the reference s.truncate(0); sl = AB_Transaction_GetPurpose(t); if (sl) { GWEN_STRINGLISTENTRY *se; bool insertLineSep = false; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); if (insertLineSep) s += '\n'; insertLineSep = true; s += QString::fromUtf8(p).trimmed(); memo += QString::fromUtf8(p).trimmed(); se = GWEN_StringListEntry_Next(se); } // while // Sparda / Netbank hack: the software these banks use stores // parts of the payee name in the beginning of the purpose field // in case the payee name exceeds the 27 character limit. This is // the case, when one of the strings listed in m_sepaKeywords is part // of the purpose fields but does not start at the beginning. In this // case, the part leading up to the keyword is to be treated as the // tail of the payee. Also, a blank is inserted after the keyword. QSet::const_iterator itk; for (itk = m_sepaKeywords.constBegin(); itk != m_sepaKeywords.constEnd(); ++itk) { int idx = s.indexOf(*itk); if (idx >= 0) { if (idx > 0) { // re-add a possibly removed blank to name if (kt.m_strPayee.length() < 27) kt.m_strPayee += ' '; kt.m_strPayee += s.left(idx); s = s.mid(idx); } s = QString("%1 %2").arg(*itk).arg(s.mid((*itk).length())); // now do the same for 'memo' except for updating the payee idx = memo.indexOf(*itk); if (idx >= 0) { if (idx > 0) { memo = memo.mid(idx); } } memo = QString("%1 %2").arg(*itk).arg(memo.mid((*itk).length())); break; } } // in case we have some SEPA fields filled with information // we add them to the memo field p = AB_Transaction_GetEndToEndReference(t); if (p) { s += QString(", EREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("EREF: %1").arg(p)); } p = AB_Transaction_GetCustomerReference(t); if (p) { s += QString(", CREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CREF: %1").arg(p)); } p = AB_Transaction_GetMandateId(t); if (p) { s += QString(", MREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("MREF: %1").arg(p)); } p = AB_Transaction_GetCreditorSchemeId(t); if (p) { s += QString(", CRED: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CRED: %1").arg(p)); } p = AB_Transaction_GetOriginatorIdentifier(t); if (p) { s += QString(", DEBT: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("DEBT: %1").arg(p)); } } const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); // check if we need the version with or without linebreaks if (kvp.value("kbanking-memo-removelinebreaks").compare(QLatin1String("no"))) { kt.m_strMemo = memo; } else { kt.m_strMemo = s; } // calculate the hash code and start with the payee info // and append the memo field h = MyMoneyTransaction::hash(kt.m_strPayee.trimmed()); h = MyMoneyTransaction::hash(s, h); // see, if we need to extract the payee from the memo field QString rePayee = kvp.value("kbanking-payee-regexp"); if (!rePayee.isEmpty() && kt.m_strPayee.isEmpty()) { QString reMemo = kvp.value("kbanking-memo-regexp"); QStringList exceptions = kvp.value("kbanking-payee-exceptions").split(';', QString::SkipEmptyParts); bool needExtract = true; QStringList::const_iterator it_s; for (it_s = exceptions.constBegin(); needExtract && it_s != exceptions.constEnd(); ++it_s) { QRegExp exp(*it_s, Qt::CaseInsensitive); if (exp.indexIn(kt.m_strMemo) != -1) { needExtract = false; } } if (needExtract) { QRegExp expPayee(rePayee, Qt::CaseInsensitive); QRegExp expMemo(reMemo, Qt::CaseInsensitive); if (expPayee.indexIn(kt.m_strMemo) != -1) { kt.m_strPayee = expPayee.cap(1); if (expMemo.indexIn(kt.m_strMemo) != -1) { kt.m_strMemo = expMemo.cap(1); } } } } kt.m_strPayee = kt.m_strPayee.trimmed(); // date ti = AB_Transaction_GetDate(t); if (!ti) ti = AB_Transaction_GetValutaDate(t); if (ti) { int year, month, day; if (!startTime) startTime = ti; /*else { dead code if (GWEN_Time_Diff(ti, startTime) < 0) startTime = ti; }*/ if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) { kt.m_datePosted = QDate(year, month + 1, day); } } else { DBG_WARN(0, "No date for transaction"); } // value val = AB_Transaction_GetValue(t); if (val) { if (ks.m_strCurrency.isEmpty()) { p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } else { p = AB_Value_GetCurrency(val); if (p) s = p; if (ks.m_strCurrency.toLower() != s.toLower()) { // TODO: handle currency difference DBG_ERROR(0, "Mixed currencies currently not allowed"); } } kt.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val)); // The initial implementation of this feature was based on // a denominator of 100. Since the denominator might be // different nowadays, we make sure to use 100 for the // duplicate detection QString tmpVal = kt.m_amount.formatMoney(100, false); tmpVal.remove(QRegExp("[,\\.]")); tmpVal += QLatin1String("/100"); h = MyMoneyTransaction::hash(tmpVal, h); } else { DBG_WARN(0, "No value for transaction"); } if (startTime) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(startTime, &day, &month, &year)) { QDate d(year, month + 1, day); if (!ks.m_dateBegin.isValid()) ks.m_dateBegin = d; else if (d < ks.m_dateBegin) ks.m_dateBegin = d; if (!ks.m_dateEnd.isValid()) ks.m_dateEnd = d; else if (d > ks.m_dateEnd) ks.m_dateEnd = d; } } else { DBG_WARN(0, "No date in current transaction"); } // add information about remote account to memo in case we have something const char *remoteAcc = AB_Transaction_GetRemoteAccountNumber(t); const char *remoteBankCode = AB_Transaction_GetRemoteBankCode(t); if (remoteAcc && remoteBankCode) { kt.m_strMemo += QString("\n%1/%2").arg(remoteBankCode, remoteAcc); } // make hash value unique in case we don't have one already if (kt.m_strBankID.isEmpty()) { QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(kt.m_datePosted.toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = m_hashMap.constFind(hash); if (it == m_hashMap.constEnd()) { m_hashMap[hash] = true; break; } ++idx; } kt.m_strBankID = QString("%1-%2").arg(acc.id()).arg(hash); } // store transaction ks.m_listTransactions += kt; } bool KBankingExt::importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t /*flags*/) { const char *p; DBG_INFO(0, "Importing account..."); // account number MyMoneyStatement ks; p = AB_ImExporterAccountInfo_GetAccountNumber(ai); if (p) { ks.m_strAccountNumber = m_parent->stripLeadingZeroes(p); } p = AB_ImExporterAccountInfo_GetBankCode(ai); if (p) { ks.m_strRoutingNumber = m_parent->stripLeadingZeroes(p); } MyMoneyAccount kacc = m_parent->account("kbanking-acc-ref", QString("%1-%2").arg(ks.m_strRoutingNumber, ks.m_strAccountNumber)); ks.m_accountId = kacc.id(); // account name p = AB_ImExporterAccountInfo_GetAccountName(ai); if (p) ks.m_strAccountName = p; // account type switch (AB_ImExporterAccountInfo_GetType(ai)) { case AB_AccountType_Bank: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_CreditCard: ks.m_eType = eMyMoney::Statement::Type::CreditCard; break; case AB_AccountType_Checking: ks.m_eType = eMyMoney::Statement::Type::Checkings; break; case AB_AccountType_Savings: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_Investment: ks.m_eType = eMyMoney::Statement::Type::Investment; break; case AB_AccountType_Cash: default: ks.m_eType = eMyMoney::Statement::Type::None; } // account status const AB_ACCOUNT_STATUS* ast = _getAccountStatus(ai); if (ast) { const AB_BALANCE *bal; bal = AB_AccountStatus_GetBookedBalance(ast); if (!bal) bal = AB_AccountStatus_GetNotedBalance(ast); if (bal) { const AB_VALUE* val = AB_Balance_GetValue(bal); if (val) { DBG_INFO(0, "Importing balance"); ks.m_closingBalance = AB_Value_toMyMoneyMoney(val); p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } const GWEN_TIME* ti = AB_Balance_GetTime(bal); if (ti) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) ks.m_dateEnd = QDate(year, month + 1, day); } else { DBG_WARN(0, "No time for balance"); } } else { DBG_WARN(0, "No account balance"); } } else { DBG_WARN(0, "No account status"); } // clear hash map m_hashMap.clear(); // get all transactions const AB_TRANSACTION* t = AB_ImExporterAccountInfo_GetFirstTransaction(ai); while (t) { _xaToStatement(ks, kacc, t); t = AB_ImExporterAccountInfo_GetNextTransaction(ai); } // import them if (!m_parent->importStatement(ks)) { if (KMessageBox::warningYesNo(0, i18n("Error importing statement. Do you want to continue?"), i18n("Critical Error")) == KMessageBox::No) { DBG_ERROR(0, "User aborted"); return false; } } return true; } K_PLUGIN_FACTORY_WITH_JSON(KBankingFactory, "kbanking.json", registerPlugin();) #include "kbanking.moc" diff --git a/kmymoney/plugins/views/forecast/kforecastview_p.h b/kmymoney/plugins/views/forecast/kforecastview_p.h index 64045a2da..ae3172055 100644 --- a/kmymoney/plugins/views/forecast/kforecastview_p.h +++ b/kmymoney/plugins/views/forecast/kforecastview_p.h @@ -1,1025 +1,1027 @@ /*************************************************************************** kforecastview.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KFORECASTVIEW_P_H #define KFORECASTVIEW_P_H #include "kforecastview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kforecastview.h" #include "forecastviewsettings.h" #include "kmymoneyviewbase_p.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "kmymoneysettings.h" #include "mymoneybudget.h" #include "fixedcolumntreeview.h" #include "icons.h" #include "mymoneyenums.h" #include "kmymoneyutils.h" #include "kmymoneyplugin.h" #include "plugins/views/reports/reportsviewenums.h" using namespace Icons; typedef enum { SummaryView = 0, ListView, AdvancedView, BudgetView, ChartView, // insert new values above this line MaxViewTabs } ForecastViewTab; enum ForecastViewRoles { ForecastRole = Qt::UserRole, /**< The forecast is held in this role.*/ AccountRole = Qt::UserRole + 1, /**< The MyMoneyAccount is stored in this role in column 0.*/ AmountRole = Qt::UserRole + 2, /**< The amount.*/ ValueRole = Qt::UserRole + 3, /**< The value.*/ }; enum EForecastViewType { eSummary = 0, eDetailed, eAdvanced, eBudget, eUndefined }; class KForecastViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KForecastView) public: explicit KForecastViewPrivate(KForecastView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KForecastView), m_needLoad(true), m_totalItem(0), m_assetItem(0), m_liabilityItem(0), m_incomeItem(0), m_expenseItem(0), m_chartLayout(0), m_forecastChart(nullptr) { } ~KForecastViewPrivate() { delete ui; } void init() { Q_Q(KForecastView); m_needLoad = false; ui->setupUi(q); for (int i = 0; i < MaxViewTabs; ++i) m_needReload[i] = false; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); ui->m_tab->setCurrentIndex(grp.readEntry("KForecastView_LastType", 0)); ui->m_forecastButton->setIcon(Icons::get(Icon::ViewForecast)); q->connect(ui->m_tab, &QTabWidget::currentChanged, q, &KForecastView::slotTabChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KForecastView::refresh); q->connect(ui->m_forecastButton, &QAbstractButton::clicked, q, &KForecastView::slotManualForecast); ui->m_forecastList->setUniformRowHeights(true); ui->m_forecastList->setAllColumnsShowFocus(true); ui->m_summaryList->setAllColumnsShowFocus(true); ui->m_budgetList->setAllColumnsShowFocus(true); ui->m_advancedList->setAlternatingRowColors(true); q->connect(ui->m_forecastList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_forecastList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_summaryList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_summaryList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_budgetList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_budgetList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); m_chartLayout = ui->m_tabChart->layout(); m_chartLayout->setSpacing(6); loadForecastSettings(); } void loadForecast(ForecastViewTab tab) { if (m_needReload[tab]) { switch (tab) { case ListView: loadListView(); break; case SummaryView: loadSummaryView(); break; case AdvancedView: loadAdvancedView(); break; case BudgetView: loadBudgetView(); break; case ChartView: loadChartView(); break; default: break; } m_needReload[tab] = false; } } void loadListView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); ui->m_forecastList->clear(); ui->m_forecastList->setColumnCount(0); ui->m_forecastList->setIconSize(QSize(22, 22)); ui->m_forecastList->setSortingEnabled(true); ui->m_forecastList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); //add cycle interval columns headerLabels << i18nc("Today's forecast", "Current"); for (int i = 1; i <= forecast.forecastDays(); ++i) { QDate forecastDate = QDate::currentDate().addDays(i); headerLabels << QLocale().toString(forecastDate, QLocale::LongFormat); } //add variation columns headerLabels << i18n("Total variation"); //set the columns ui->m_forecastList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_forecastList, forecast); addAssetLiabilityRows(forecast); //load asset and liability forecast accounts loadAccounts(forecast, file->asset(), m_assetItem, eDetailed); loadAccounts(forecast, file->liability(), m_liabilityItem, eDetailed); adjustHeadersAndResizeToContents(ui->m_forecastList); // add the fixed column only if the horizontal scroll bar is visible m_fixedColumnView.reset(ui->m_forecastList->horizontalScrollBar()->isVisible() ? new FixedColumnTreeView(ui->m_forecastList) : 0); } void loadAccounts(MyMoneyForecast& forecast, const MyMoneyAccount& account, QTreeWidgetItem* parentItem, int forecastType) { QMap nameIdx; const auto file = MyMoneyFile::instance(); QTreeWidgetItem *forecastItem = 0; //Get all accounts of the right type to calculate forecast const auto accList = account.accountList(); if (accList.isEmpty()) return; foreach (const auto sAccount, accList) { auto subAccount = file->account(sAccount); //only add the account if it is a forecast account or the parent of a forecast account if (includeAccount(forecast, subAccount)) { nameIdx[subAccount.id()] = subAccount.id(); } } QMap::ConstIterator it_nc; for (it_nc = nameIdx.constBegin(); it_nc != nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount subAccount = file->account(*it_nc); MyMoneySecurity currency; if (subAccount.isInvest()) { MyMoneySecurity underSecurity = file->security(subAccount.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(subAccount.currencyId()); } forecastItem = new QTreeWidgetItem(parentItem); forecastItem->setText(0, subAccount.name()); forecastItem->setIcon(0, subAccount.accountPixmap()); forecastItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); forecastItem->setData(0, AccountRole, QVariant::fromValue(subAccount)); forecastItem->setExpanded(true); switch (forecastType) { case eSummary: updateSummary(forecastItem); break; case eDetailed: updateDetailed(forecastItem); break; case EForecastViewType::eBudget: updateBudget(forecastItem); break; default: break; } loadAccounts(forecast, subAccount, forecastItem, forecastType); } } void loadSummaryView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); QList accList; const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //add columns QStringList headerLabels; headerLabels << i18n("Account"); headerLabels << i18nc("Today's forecast", "Current"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle qint64 daysToBeginDay; if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } for (auto i = 0; ((i*forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { auto intervalDays = ((i * forecast.accountsCycle()) + daysToBeginDay); headerLabels << i18np("1 day", "%1 days", intervalDays); } //add variation columns headerLabels << i18n("Total variation"); ui->m_summaryList->clear(); //set the columns ui->m_summaryList->setHeaderLabels(headerLabels); ui->m_summaryList->setIconSize(QSize(22, 22)); ui->m_summaryList->setSortingEnabled(true); ui->m_summaryList->sortByColumn(0, Qt::AscendingOrder); //add default rows addTotalRow(ui->m_summaryList, forecast); addAssetLiabilityRows(forecast); loadAccounts(forecast, file->asset(), m_assetItem, eSummary); loadAccounts(forecast, file->liability(), m_liabilityItem, eSummary); adjustHeadersAndResizeToContents(ui->m_summaryList); //Add comments to the advice list ui->m_adviceText->clear(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } //Check if the account is going to be below zero or below the minimal balance in the forecast period QString minimumBalance = acc.value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); //Check if the account is going to be below minimal balance auto dropMinimum = forecast.daysToMinimumBalance(acc); //Check if the account is going to be below zero in the future auto dropZero = forecast.daysToZeroBalance(acc); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below the minimum balance %2 today.", acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); break; default: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } break; default: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } //advice about trends msg.clear(); MyMoneyMoney accCycleVariation = forecast.accountCycleVariation(acc); if (accCycleVariation < MyMoneyMoney()) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The account %1 is decreasing %2 per cycle.", acc.name(), MyMoneyUtils::formatMoney(accCycleVariation, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } ui->m_adviceText->show(); } void loadAdvancedView() { const auto file = MyMoneyFile::instance(); QList accList; MyMoneySecurity baseCurrency = file->baseCurrency(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); qint64 daysToBeginDay; //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } //clear the list, including columns ui->m_advancedList->clear(); ui->m_advancedList->setColumnCount(0); ui->m_advancedList->setIconSize(QSize(22, 22)); QStringList headerLabels; //add first column of both lists headerLabels << i18n("Account"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } //add columns for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Min Bal %1", i); headerLabels << i18n("Min Date %1", i); } for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Max Bal %1", i); headerLabels << i18n("Max Date %1", i); } headerLabels << i18nc("Average balance", "Average"); ui->m_advancedList->setHeaderLabels(headerLabels); QTreeWidgetItem *advancedItem = 0; QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); QString amount; MyMoneyMoney amountMM; MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } advancedItem = new QTreeWidgetItem(ui->m_advancedList, advancedItem, false); advancedItem->setText(0, acc.name()); advancedItem->setIcon(0, acc.accountPixmap()); auto it_c = 1; // iterator for the columns of the listview //get minimum balance list QList minBalanceList = forecast.accountMinimumBalanceDateList(acc); QList::Iterator t_min; for (t_min = minBalanceList.begin(); t_min != minBalanceList.end() ; ++t_min) { QDate minDate = *t_min; amountMM = forecast.forecastBalance(acc, minDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(minDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get maximum balance list QList maxBalanceList = forecast.accountMaximumBalanceDateList(acc); QList::Iterator t_max; for (t_max = maxBalanceList.begin(); t_max != maxBalanceList.end() ; ++t_max) { QDate maxDate = *t_max; amountMM = forecast.forecastBalance(acc, maxDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(maxDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get average balance amountMM = forecast.accountAverageBalance(acc); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } // make sure all data is shown adjustHeadersAndResizeToContents(ui->m_advancedList); ui->m_advancedList->show(); } void loadBudgetView() { const auto file = MyMoneyFile::instance(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); //get the settings from current page and calculate this year based on last year QDate historyEndDate = QDate(QDate::currentDate().year() - 1, 12, 31); QDate historyStartDate = historyEndDate.addDays(-ui->m_accountsCycle->value() * ui->m_forecastCycles->value()); QDate forecastStartDate = QDate(QDate::currentDate().year(), 1, 1); QDate forecastEndDate = QDate::currentDate().addDays(ui->m_forecastDays->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); MyMoneyBudget budget; forecast.createBudget(budget, historyStartDate, historyEndDate, forecastStartDate, forecastEndDate, false); ui->m_budgetList->clear(); ui->m_budgetList->setIconSize(QSize(22, 22)); ui->m_budgetList->setSortingEnabled(true); ui->m_budgetList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); { forecastStartDate = forecast.forecastStartDate(); forecastEndDate = forecast.forecastEndDate(); //add cycle interval columns QDate f_date = forecastStartDate; for (; f_date <= forecastEndDate; f_date = f_date.addMonths(1)) { headerLabels << QDate::longMonthName(f_date.month()); } } //add total column headerLabels << i18nc("Total balance", "Total"); //set the columns ui->m_budgetList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_budgetList, forecast); addIncomeExpenseRows(forecast); //load income and expense budget accounts loadAccounts(forecast, file->income(), m_incomeItem, EForecastViewType::eBudget); loadAccounts(forecast, file->expense(), m_expenseItem, EForecastViewType::eBudget); adjustHeadersAndResizeToContents(ui->m_budgetList); } void loadChartView() { if (m_forecastChart) delete m_forecastChart; if (const auto reportsPlugin = pPlugins.data.value("reportsview", nullptr)) { const QString args = QString::number(ui->m_comboDetail->currentIndex()) + ';' + QString::number(ui->m_forecastDays->value()) + ';' + QString::number(ui->m_tab->width()) + ';' + QString::number(ui->m_tab->height()); - const auto variantReport = reportsPlugin->requestData(args, eWidgetPlugin::WidgetType::NetWorthForecastWithArgs); + const auto variantReport = reportsPlugin->requestData(args, eWidgetPlugin::WidgetType::NetWorthForecastWithArgs); if (!variantReport.isNull()) m_forecastChart = variantReport.value(); + else + m_forecastChart = new QLabel(i18n("No data provided by reports plugin for this chart.")); } else { m_forecastChart = new QLabel(i18n("Enable reports plugin to see this chart.")); } m_chartLayout->addWidget(m_forecastChart); } void loadForecastSettings() { //fill the settings controls ui->m_forecastDays->setValue(KMyMoneySettings::forecastDays()); ui->m_accountsCycle->setValue(KMyMoneySettings::forecastAccountCycle()); ui->m_beginDay->setValue(KMyMoneySettings::beginForecastDay()); ui->m_forecastCycles->setValue(KMyMoneySettings::forecastCycles()); ui->m_historyMethod->setId(ui->radioButton11, 0); // simple moving avg ui->m_historyMethod->setId(ui->radioButton12, 1); // weighted moving avg ui->m_historyMethod->setId(ui->radioButton13, 2); // linear regression ui->m_historyMethod->button(KMyMoneySettings::historyMethod())->setChecked(true); switch (KMyMoneySettings::forecastMethod()) { case 0: ui->m_forecastMethod->setText(i18nc("Scheduled method", "Scheduled")); ui->m_forecastCycles->setDisabled(true); ui->m_historyMethodGroupBox->setDisabled(true); break; case 1: ui->m_forecastMethod->setText(i18nc("History-based method", "History")); ui->m_forecastCycles->setEnabled(true); ui->m_historyMethodGroupBox->setEnabled(true); break; default: ui->m_forecastMethod->setText(i18nc("Unknown forecast method", "Unknown")); break; } } void addAssetLiabilityRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_assetItem = new QTreeWidgetItem(m_totalItem); m_assetItem->setText(0, file->asset().name()); m_assetItem->setIcon(0, file->asset().accountPixmap()); m_assetItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_assetItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_assetItem->setExpanded(true); m_liabilityItem = new QTreeWidgetItem(m_totalItem); m_liabilityItem->setText(0, file->liability().name()); m_liabilityItem->setIcon(0, file->liability().accountPixmap()); m_liabilityItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_liabilityItem->setData(0, AccountRole, QVariant::fromValue(file->liability())); m_liabilityItem->setExpanded(true); } void addIncomeExpenseRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_incomeItem = new QTreeWidgetItem(m_totalItem); m_incomeItem->setText(0, file->income().name()); m_incomeItem->setIcon(0, file->income().accountPixmap()); m_incomeItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_incomeItem->setData(0, AccountRole, QVariant::fromValue(file->income())); m_incomeItem->setExpanded(true); m_expenseItem = new QTreeWidgetItem(m_totalItem); m_expenseItem->setText(0, file->expense().name()); m_expenseItem->setIcon(0, file->expense().accountPixmap()); m_expenseItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_expenseItem->setData(0, AccountRole, QVariant::fromValue(file->expense())); m_expenseItem->setExpanded(true); } void addTotalRow(QTreeWidget* forecastList, const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_totalItem = new QTreeWidgetItem(forecastList); QFont font; font.setBold(true); m_totalItem->setFont(0, font); m_totalItem->setText(0, i18nc("Total balance", "Total")); m_totalItem->setIcon(0, file->asset().accountPixmap()); m_totalItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_totalItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_totalItem->setExpanded(true); } bool includeAccount(MyMoneyForecast& forecast, const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); if (forecast.isForecastAccount(acc)) return true; foreach (const auto sAccount, acc.accountList()) { auto account = file->account(sAccount); if (includeAccount(forecast, account)) return true; } return false; } void adjustHeadersAndResizeToContents(QTreeWidget *widget) { QSize sizeHint(0, widget->sizeHintForRow(0)); QTreeWidgetItem *header = widget->headerItem(); for (int i = 0; i < header->columnCount(); ++i) { if (i > 0) { header->setData(i, Qt::TextAlignmentRole, Qt::AlignRight); // make sure that the row height stays the same even when the column that has icons is not visible if (m_totalItem) { m_totalItem->setSizeHint(i, sizeHint); } } widget->resizeColumnToContents(i); } } void setNegative(QTreeWidgetItem *item, bool isNegative) { if (isNegative) { for (int i = 0; i < item->columnCount(); ++i) { item->setForeground(i, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } } void showAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const MyMoneySecurity& security) { item->setText(column, MyMoneyUtils::formatMoney(amount, security)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); item->setFont(column, item->font(0)); if (amount.isNegative()) { item->setForeground(column, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } void adjustParentValue(QTreeWidgetItem *item, int column, const MyMoneyMoney& value) { if (!item) return; item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + value)); item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value().convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()))); // if the entry has no children, // or it is the top entry // or it is currently not open // we need to display the value of it if (item->childCount() == 0 || !item->parent() || (!item->isExpanded() && item->childCount() > 0) || (item->parent() && !item->parent()->parent())) { if (item->childCount() > 0) item->setText(column, " "); MyMoneyMoney amount = item->data(column, ValueRole).value(); showAmount(item, column, amount, MyMoneyFile::instance()->baseCurrency()); } // now make sure, the upstream accounts also get notified about the value change adjustParentValue(item->parent(), column, value); } void setValue(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const QDate& forecastDate) { MyMoneyAccount account = item->data(0, AccountRole).value(); //calculate the balance in base currency for the total row if (account.currencyId() != MyMoneyFile::instance()->baseCurrency().id()) { const auto file = MyMoneyFile::instance(); const auto curPrice = file->price(account.tradingCurrencyId(), file->baseCurrency().id(), forecastDate); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseAmountMM = amount * curRate; auto value = baseAmountMM.convert(file->baseCurrency().smallestAccountFraction()); item->setData(column, ValueRole, QVariant::fromValue(value)); adjustParentValue(item->parent(), column, value); } else { item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + amount)); adjustParentValue(item->parent(), column, amount); } } void setAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount) { item->setData(column, AmountRole, QVariant::fromValue(amount)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); } void updateSummary(QTreeWidgetItem *item) { MyMoneyMoney amountMM; auto it_c = 1; // iterator for the columns of the listview const auto file = MyMoneyFile::instance(); qint64 daysToBeginDay; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //add current balance column QDate summaryDate = QDate::currentDate(); amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); it_c++; //iterate through all other columns for (summaryDate = QDate::currentDate().addDays(daysToBeginDay); summaryDate <= forecast.forecastEndDate(); summaryDate = summaryDate.addDays(forecast.accountsCycle()), ++it_c) { amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle setNegative(item, forecast.accountTotalVariation(account).isNegative()); setAmount(item, it_c, forecast.accountTotalVariation(account)); setValue(item, it_c, forecast.accountTotalVariation(account), forecast.forecastEndDate()); showAmount(item, it_c, forecast.accountTotalVariation(account), currency); } void updateDetailed(QTreeWidgetItem *item) { MyMoneyMoney vAmountMM; const auto file = MyMoneyFile::instance(); MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } int it_c = 1; // iterator for the columns of the listview MyMoneyForecast forecast = item->data(0, ForecastRole).value(); for (QDate forecastDate = QDate::currentDate(); forecastDate <= forecast.forecastEndDate(); ++it_c, forecastDate = forecastDate.addDays(1)) { MyMoneyMoney amountMM = forecast.forecastBalance(account, forecastDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle vAmountMM = forecast.accountTotalVariation(account); setAmount(item, it_c, vAmountMM); setValue(item, it_c, vAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, vAmountMM, currency); } void updateBudget(QTreeWidgetItem *item) { MyMoneySecurity currency; MyMoneyMoney tAmountMM; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); const auto file = MyMoneyFile::instance(); int it_c = 1; // iterator for the columns of the listview QDate forecastDate = forecast.forecastStartDate(); MyMoneyAccount account = item->data(0, AccountRole).value(); if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //iterate columns for (; forecastDate <= forecast.forecastEndDate(); forecastDate = forecastDate.addMonths(1), ++it_c) { MyMoneyMoney amountMM; amountMM = forecast.forecastBalance(account, forecastDate); if (account.accountType() == eMyMoney::Account::Type::Expense) amountMM = -amountMM; tAmountMM += amountMM; setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //set total column setAmount(item, it_c, tAmountMM); setValue(item, it_c, tAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, tAmountMM, currency); } /** * Get the list of prices for an account * This is used later to create an instance of KMyMoneyAccountTreeForecastItem * */ // QList getAccountPrices(const MyMoneyAccount& acc) // { // const auto file = MyMoneyFile::instance(); // QList prices; // MyMoneySecurity security = file->baseCurrency(); // try { // if (acc.isInvest()) { // security = file->security(acc.currencyId()); // if (security.tradingCurrency() != file->baseCurrency().id()) { // MyMoneySecurity sec = file->security(security.tradingCurrency()); // prices += file->price(sec.id(), file->baseCurrency().id()); // } // } else if (acc.currencyId() != file->baseCurrency().id()) { // if (acc.currencyId() != file->baseCurrency().id()) { // security = file->security(acc.currencyId()); // prices += file->price(acc.currencyId(), file->baseCurrency().id()); // } // } // } catch (const MyMoneyException &e) { // qDebug() << Q_FUNC_INFO << " caught exception while adding " << acc.name() << "[" << acc.id() << "]: " << e.what(); // } // return prices; // } KForecastView *q_ptr; Ui::KForecastView *ui; bool m_needReload[MaxViewTabs]; /** * This member holds the load state of page */ bool m_needLoad; QTreeWidgetItem* m_totalItem; QTreeWidgetItem* m_assetItem; QTreeWidgetItem* m_liabilityItem; QTreeWidgetItem* m_incomeItem; QTreeWidgetItem* m_expenseItem; QLayout* m_chartLayout; QWidget *m_forecastChart; QScopedPointer m_fixedColumnView; QMap m_nameIdx; }; #endif diff --git a/kmymoney/plugins/views/reports/reportsview.cpp b/kmymoney/plugins/views/reports/reportsview.cpp index 98a651be4..5953fe67a 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()); - auto isOverrun = false; + 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) { 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/xmlhelper/xmlstoragehelper.cpp b/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp index 04cf765d0..487bae657 100644 --- a/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp +++ b/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp @@ -1,1254 +1,1249 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2007-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * Copyright 2019 Thomas Baumgart * * 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 "xmlstoragehelper.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneytransactionfilter.h" #include "mymoneyenums.h" #include "mymoneyexception.h" namespace Element { enum class Report { Payee, Tag, Account, Text, Type, State, Number, Amount, Dates, Category, AccountGroup, Validity }; enum class Budget { Budget = 0, Account, Period }; } namespace Attribute { enum class Report { ID = 0, Group, Type, Name, Comment, ConvertCurrency, Favorite, SkipZero, DateLock, DataLock, MovingAverageDays, IncludesActuals, IncludesForecast, IncludesPrice, IncludesAveragePrice, IncludesMovingAverage, IncludesSchedules, IncludesTransfers, IncludesUnused, MixedTime, Investments, Budget, ShowRowTotals, ShowColumnTotals, Detail, ColumnsAreDays, ChartType, ChartCHGridLines, ChartSVGridLines, ChartDataLabels, ChartByDefault, LogYAxis, ChartLineWidth, ColumnType, RowType, DataRangeStart, DataRangeEnd, DataMajorTick, DataMinorTick, YLabelsPrecision, QueryColumns, Tax, Loans, HideTransactions, InvestmentSum, SettlementPeriod, ShowSTLTCapitalGains, TermsSeparator, Pattern, CaseSensitive, RegEx, InvertText, State, From, To, Validity, // insert new entries above this line LastAttribute }; enum class Budget { ID = 0, Name, Start, Version, BudgetLevel, BudgetSubAccounts, Amount, // insert new entries above this line LastAttribute }; } namespace MyMoneyXmlContentHandler2 { enum class Node { Report, Budget }; QString nodeName(Node nodeID) { static const QHash nodeNames { {Node::Report, QStringLiteral("REPORT")}, {Node::Budget, QStringLiteral("BUDGET")} }; return nodeNames.value(nodeID); } uint qHash(const Node key, uint seed) { return ::qHash(static_cast(key), seed); } QString elementName(Element::Report elementID) { static const QMap elementNames { {Element::Report::Payee, QStringLiteral("PAYEE")}, {Element::Report::Tag, QStringLiteral("TAG")}, {Element::Report::Account, QStringLiteral("ACCOUNT")}, {Element::Report::Text, QStringLiteral("TEXT")}, {Element::Report::Type, QStringLiteral("TYPE")}, {Element::Report::State, QStringLiteral("STATE")}, {Element::Report::Number, QStringLiteral("NUMBER")}, {Element::Report::Amount, QStringLiteral("AMOUNT")}, {Element::Report::Dates, QStringLiteral("DATES")}, {Element::Report::Category, QStringLiteral("CATEGORY")}, {Element::Report::AccountGroup, QStringLiteral("ACCOUNTGROUP")}, {Element::Report::Validity, QStringLiteral("VALIDITY")} }; return elementNames.value(elementID); } QString attributeName(Attribute::Report attributeID) { static const QMap attributeNames { {Attribute::Report::ID, QStringLiteral("id")}, {Attribute::Report::Group, QStringLiteral("group")}, {Attribute::Report::Type, QStringLiteral("type")}, {Attribute::Report::Name, QStringLiteral("name")}, {Attribute::Report::Comment, QStringLiteral("comment")}, {Attribute::Report::ConvertCurrency, QStringLiteral("convertcurrency")}, {Attribute::Report::Favorite, QStringLiteral("favorite")}, {Attribute::Report::SkipZero, QStringLiteral("skipZero")}, {Attribute::Report::DateLock, QStringLiteral("datelock")}, {Attribute::Report::DataLock, QStringLiteral("datalock")}, {Attribute::Report::MovingAverageDays, QStringLiteral("movingaveragedays")}, {Attribute::Report::IncludesActuals, QStringLiteral("includesactuals")}, {Attribute::Report::IncludesForecast, QStringLiteral("includesforecast")}, {Attribute::Report::IncludesPrice, QStringLiteral("includesprice")}, {Attribute::Report::IncludesAveragePrice, QStringLiteral("includesaverageprice")}, {Attribute::Report::IncludesMovingAverage, QStringLiteral("includesmovingaverage")}, {Attribute::Report::IncludesSchedules, QStringLiteral("includeschedules")}, {Attribute::Report::IncludesTransfers, QStringLiteral("includestransfers")}, {Attribute::Report::IncludesUnused, QStringLiteral("includeunused")}, {Attribute::Report::MixedTime, QStringLiteral("mixedtime")}, {Attribute::Report::Investments, QStringLiteral("investments")}, {Attribute::Report::Budget, QStringLiteral("budget")}, {Attribute::Report::ShowRowTotals, QStringLiteral("showrowtotals")}, {Attribute::Report::ShowColumnTotals, QStringLiteral("showcolumntotals")}, {Attribute::Report::Detail, QStringLiteral("detail")}, {Attribute::Report::ColumnsAreDays, QStringLiteral("columnsaredays")}, {Attribute::Report::ChartType, QStringLiteral("charttype")}, {Attribute::Report::ChartCHGridLines, QStringLiteral("chartchgridlines")}, {Attribute::Report::ChartSVGridLines, QStringLiteral("chartsvgridlines")}, {Attribute::Report::ChartDataLabels, QStringLiteral("chartdatalabels")}, {Attribute::Report::ChartByDefault, QStringLiteral("chartbydefault")}, {Attribute::Report::LogYAxis, QStringLiteral("logYaxis")}, {Attribute::Report::ChartLineWidth, QStringLiteral("chartlinewidth")}, {Attribute::Report::ColumnType, QStringLiteral("columntype")}, {Attribute::Report::RowType, QStringLiteral("rowtype")}, {Attribute::Report::DataRangeStart, QStringLiteral("dataRangeStart")}, {Attribute::Report::DataRangeEnd, QStringLiteral("dataRangeEnd")}, {Attribute::Report::DataMajorTick, QStringLiteral("dataMajorTick")}, {Attribute::Report::DataMinorTick, QStringLiteral("dataMinorTick")}, {Attribute::Report::YLabelsPrecision, QStringLiteral("yLabelsPrecision")}, {Attribute::Report::QueryColumns, QStringLiteral("querycolumns")}, {Attribute::Report::Tax, QStringLiteral("tax")}, {Attribute::Report::Loans, QStringLiteral("loans")}, {Attribute::Report::HideTransactions, QStringLiteral("hidetransactions")}, {Attribute::Report::InvestmentSum, QStringLiteral("investmentsum")}, {Attribute::Report::SettlementPeriod, QStringLiteral("settlementperiod")}, {Attribute::Report::ShowSTLTCapitalGains, QStringLiteral("showSTLTCapitalGains")}, {Attribute::Report::TermsSeparator, QStringLiteral("tseparator")}, {Attribute::Report::Pattern, QStringLiteral("pattern")}, {Attribute::Report::CaseSensitive, QStringLiteral("casesensitive")}, {Attribute::Report::RegEx, QStringLiteral("regex")}, {Attribute::Report::InvertText, QStringLiteral("inverttext")}, {Attribute::Report::State, QStringLiteral("state")}, {Attribute::Report::From, QStringLiteral("from")}, {Attribute::Report::To, QStringLiteral("to")}, {Attribute::Report::Validity, QStringLiteral("validity")} }; return attributeNames.value(attributeID); } QString elementName(Element::Budget elementID) { static const QMap elementNames { {Element::Budget::Budget, QStringLiteral("BUDGET")}, {Element::Budget::Account, QStringLiteral("ACCOUNT")}, {Element::Budget::Period, QStringLiteral("PERIOD")} }; return elementNames.value(elementID); } QString attributeName(Attribute::Budget attributeID) { static const QMap attributeNames { {Attribute::Budget::ID, QStringLiteral("id")}, {Attribute::Budget::Name, QStringLiteral("name")}, {Attribute::Budget::Start, QStringLiteral("start")}, {Attribute::Budget::Version, QStringLiteral("version")}, {Attribute::Budget::BudgetLevel, QStringLiteral("budgetlevel")}, {Attribute::Budget::BudgetSubAccounts, QStringLiteral("budgetsubaccounts")}, {Attribute::Budget::Amount, QStringLiteral("amount")} }; return attributeNames.value(attributeID); } QHash rowTypesLUT() { static const QHash lut { {eMyMoney::Report::RowType::NoRows, QStringLiteral("none")}, {eMyMoney::Report::RowType::AssetLiability, QStringLiteral("assetliability")}, {eMyMoney::Report::RowType::ExpenseIncome, QStringLiteral("expenseincome")}, {eMyMoney::Report::RowType::Category, QStringLiteral("category")}, {eMyMoney::Report::RowType::TopCategory, QStringLiteral("topcategory")}, {eMyMoney::Report::RowType::Account, QStringLiteral("account")}, {eMyMoney::Report::RowType::Tag, QStringLiteral("tag")}, {eMyMoney::Report::RowType::Payee, QStringLiteral("payee")}, {eMyMoney::Report::RowType::Month, QStringLiteral("month")}, {eMyMoney::Report::RowType::Week, QStringLiteral("week")}, {eMyMoney::Report::RowType::TopAccount, QStringLiteral("topaccount")}, {eMyMoney::Report::RowType::AccountByTopAccount, QStringLiteral("topaccount-account")}, {eMyMoney::Report::RowType::EquityType, QStringLiteral("equitytype")}, {eMyMoney::Report::RowType::AccountType, QStringLiteral("accounttype")}, {eMyMoney::Report::RowType::Institution, QStringLiteral("institution")}, {eMyMoney::Report::RowType::Budget, QStringLiteral("budget")}, {eMyMoney::Report::RowType::BudgetActual, QStringLiteral("budgetactual")}, {eMyMoney::Report::RowType::Schedule, QStringLiteral("schedule")}, {eMyMoney::Report::RowType::AccountInfo, QStringLiteral("accountinfo")}, {eMyMoney::Report::RowType::AccountLoanInfo, QStringLiteral("accountloaninfo")}, {eMyMoney::Report::RowType::AccountReconcile, QStringLiteral("accountreconcile")}, {eMyMoney::Report::RowType::CashFlow, QStringLiteral("cashflow")}, }; return lut; } QString reportNames(eMyMoney::Report::RowType textID) { return rowTypesLUT().value(textID); } eMyMoney::Report::RowType stringToRowType(const QString &text) { return rowTypesLUT().key(text, eMyMoney::Report::RowType::Invalid); } QHash columTypesLUT() { static const QHash lut { {eMyMoney::Report::ColumnType::NoColumns, QStringLiteral("none")}, {eMyMoney::Report::ColumnType::Months, QStringLiteral("months")}, {eMyMoney::Report::ColumnType::BiMonths, QStringLiteral("bimonths")}, {eMyMoney::Report::ColumnType::Quarters, QStringLiteral("quarters")}, // {eMyMoney::Report::ColumnType::, QStringLiteral("4")} // {eMyMoney::Report::ColumnType::, QStringLiteral("5")} // {eMyMoney::Report::ColumnType::, QStringLiteral("6")} {eMyMoney::Report::ColumnType::Weeks, QStringLiteral("weeks")}, // {eMyMoney::Report::ColumnType::, QStringLiteral("8")} // {eMyMoney::Report::ColumnType::, QStringLiteral("9")} // {eMyMoney::Report::ColumnType::, QStringLiteral("10")} // {eMyMoney::Report::ColumnType::, QStringLiteral("11")} {eMyMoney::Report::ColumnType::Years, QStringLiteral("years")} }; return lut; } QString reportNames(eMyMoney::Report::ColumnType textID) { return columTypesLUT().value(textID); } eMyMoney::Report::ColumnType stringToColumnType(const QString &text) { return columTypesLUT().key(text, eMyMoney::Report::ColumnType::Invalid); } QHash queryColumnsLUT() { static const QHash lut { {eMyMoney::Report::QueryColumn::None, QStringLiteral("none")}, {eMyMoney::Report::QueryColumn::Number, QStringLiteral("number")}, {eMyMoney::Report::QueryColumn::Payee, QStringLiteral("payee")}, {eMyMoney::Report::QueryColumn::Category, QStringLiteral("category")}, {eMyMoney::Report::QueryColumn::Tag, QStringLiteral("tag")}, {eMyMoney::Report::QueryColumn::Memo, QStringLiteral("memo")}, {eMyMoney::Report::QueryColumn::Account, QStringLiteral("account")}, {eMyMoney::Report::QueryColumn::Reconciled, QStringLiteral("reconcileflag")}, {eMyMoney::Report::QueryColumn::Action, QStringLiteral("action")}, {eMyMoney::Report::QueryColumn::Shares, QStringLiteral("shares")}, {eMyMoney::Report::QueryColumn::Price, QStringLiteral("price")}, {eMyMoney::Report::QueryColumn::Performance, QStringLiteral("performance")}, {eMyMoney::Report::QueryColumn::Loan, QStringLiteral("loan")}, {eMyMoney::Report::QueryColumn::Balance, QStringLiteral("balance")}, {eMyMoney::Report::QueryColumn::CapitalGain, QStringLiteral("capitalgain")} }; return lut; } QString reportNamesForQC(eMyMoney::Report::QueryColumn textID) { return queryColumnsLUT().value(textID); } eMyMoney::Report::QueryColumn stringToQueryColumn(const QString &text) { return queryColumnsLUT().key(text, eMyMoney::Report::QueryColumn::End); } QHash detailLevelLUT() { static const QHash lut { {eMyMoney::Report::DetailLevel::None, QStringLiteral("none")}, {eMyMoney::Report::DetailLevel::All, QStringLiteral("all")}, {eMyMoney::Report::DetailLevel::Top, QStringLiteral("top")}, {eMyMoney::Report::DetailLevel::Group, QStringLiteral("group")}, {eMyMoney::Report::DetailLevel::Total, QStringLiteral("total")}, {eMyMoney::Report::DetailLevel::End, QStringLiteral("invalid")} }; return lut; } QString reportNames(eMyMoney::Report::DetailLevel textID) { return detailLevelLUT().value(textID); } eMyMoney::Report::DetailLevel stringToDetailLevel(const QString &text) { return detailLevelLUT().key(text, eMyMoney::Report::DetailLevel::End); } QHash chartTypeLUT() { static const QHash lut { {eMyMoney::Report::ChartType::None, QStringLiteral("none")}, {eMyMoney::Report::ChartType::Line, QStringLiteral("line")}, {eMyMoney::Report::ChartType::Bar, QStringLiteral("bar")}, {eMyMoney::Report::ChartType::Pie, QStringLiteral("pie")}, {eMyMoney::Report::ChartType::Ring, QStringLiteral("ring")}, {eMyMoney::Report::ChartType::StackedBar, QStringLiteral("stackedbar")} }; return lut; } QString reportNames(eMyMoney::Report::ChartType textID) { return chartTypeLUT().value(textID); } eMyMoney::Report::ChartType stringToChartType(const QString &text) { return chartTypeLUT().key(text, eMyMoney::Report::ChartType::End); } QHash typeAttributeLUT() { static const QHash lut { {0, QStringLiteral("all")}, {1, QStringLiteral("payments")}, {2, QStringLiteral("deposits")}, {3, QStringLiteral("transfers")}, {4, QStringLiteral("none")}, }; return lut; } QString typeAttributeToString(int textID) { return typeAttributeLUT().value(textID); } int stringToTypeAttribute(const QString &text) { return typeAttributeLUT().key(text, 4); } QHash stateAttributeLUT() { static const QHash lut { {0, QStringLiteral("all")}, {1, QStringLiteral("notreconciled")}, {2, QStringLiteral("cleared")}, {3, QStringLiteral("reconciled")}, {4, QStringLiteral("frozen")}, {5, QStringLiteral("none")} }; return lut; } QString stateAttributeToString(int textID) { return stateAttributeLUT().value(textID); } int stringToStateAttribute(const QString &text) { return stateAttributeLUT().key(text, 5); } QHash validityAttributeLUT() { static const QHash lut { {0, QStringLiteral("any")}, {1, QStringLiteral("valid")}, {2, QStringLiteral("invalid")}, }; return lut; } QString validityAttributeToString(int textID) { return validityAttributeLUT().value(textID); } int stringToValidityAttribute(const QString &text) { return validityAttributeLUT().key(text, 0); } QHash dateLockLUT() { static const QHash lut { {eMyMoney::TransactionFilter::Date::All, QStringLiteral("alldates")}, {eMyMoney::TransactionFilter::Date::AsOfToday, QStringLiteral("untiltoday")}, {eMyMoney::TransactionFilter::Date::CurrentMonth, QStringLiteral("currentmonth")}, {eMyMoney::TransactionFilter::Date::CurrentYear, QStringLiteral("currentyear")}, {eMyMoney::TransactionFilter::Date::MonthToDate, QStringLiteral("monthtodate")}, {eMyMoney::TransactionFilter::Date::YearToDate, QStringLiteral("yeartodate")}, {eMyMoney::TransactionFilter::Date::YearToMonth, QStringLiteral("yeartomonth")}, {eMyMoney::TransactionFilter::Date::LastMonth, QStringLiteral("lastmonth")}, {eMyMoney::TransactionFilter::Date::LastYear, QStringLiteral("lastyear")}, {eMyMoney::TransactionFilter::Date::Last7Days, QStringLiteral("last7days")}, {eMyMoney::TransactionFilter::Date::Last30Days, QStringLiteral("last30days")}, {eMyMoney::TransactionFilter::Date::Last3Months, QStringLiteral("last3months")}, {eMyMoney::TransactionFilter::Date::Last6Months, QStringLiteral("last6months")}, {eMyMoney::TransactionFilter::Date::Last12Months, QStringLiteral("last12months")}, {eMyMoney::TransactionFilter::Date::Next7Days, QStringLiteral("next7days")}, {eMyMoney::TransactionFilter::Date::Next30Days, QStringLiteral("next30days")}, {eMyMoney::TransactionFilter::Date::Next3Months, QStringLiteral("next3months")}, {eMyMoney::TransactionFilter::Date::Next6Months, QStringLiteral("next6months")}, {eMyMoney::TransactionFilter::Date::Next12Months, QStringLiteral("next12months")}, {eMyMoney::TransactionFilter::Date::UserDefined, QStringLiteral("userdefined")}, {eMyMoney::TransactionFilter::Date::Last3ToNext3Months, QStringLiteral("last3tonext3months")}, {eMyMoney::TransactionFilter::Date::Last11Months, QStringLiteral("last11Months")}, {eMyMoney::TransactionFilter::Date::CurrentQuarter, QStringLiteral("currentQuarter")}, {eMyMoney::TransactionFilter::Date::LastQuarter, QStringLiteral("lastQuarter")}, {eMyMoney::TransactionFilter::Date::NextQuarter, QStringLiteral("nextQuarter")}, {eMyMoney::TransactionFilter::Date::CurrentFiscalYear, QStringLiteral("currentFiscalYear")}, {eMyMoney::TransactionFilter::Date::LastFiscalYear, QStringLiteral("lastFiscalYear")}, {eMyMoney::TransactionFilter::Date::Today, QStringLiteral("today")}, {eMyMoney::TransactionFilter::Date::Next18Months, QStringLiteral("next18months")} }; return lut; } QString dateLockAttributeToString(eMyMoney::TransactionFilter::Date textID) { return dateLockLUT().value(textID); } eMyMoney::TransactionFilter::Date stringToDateLockAttribute(const QString &text) { return dateLockLUT().key(text, eMyMoney::TransactionFilter::Date::UserDefined); } QHash dataLockLUT() { static const QHash lut { {eMyMoney::Report::DataLock::Automatic, QStringLiteral("automatic")}, {eMyMoney::Report::DataLock::UserDefined, QStringLiteral("userdefined")} }; return lut; } QString reportNames(eMyMoney::Report::DataLock textID) { return dataLockLUT().value(textID); } eMyMoney::Report::DataLock stringToDataLockAttribute(const QString &text) { return dataLockLUT().key(text, eMyMoney::Report::DataLock::DataOptionCount); } QHash accountTypeAttributeLUT() { static const QHash lut { {eMyMoney::Account::Type::Unknown, QStringLiteral("unknown")}, {eMyMoney::Account::Type::Checkings, QStringLiteral("checkings")}, {eMyMoney::Account::Type::Savings, QStringLiteral("savings")}, {eMyMoney::Account::Type::Cash, QStringLiteral("cash")}, {eMyMoney::Account::Type::CreditCard, QStringLiteral("creditcard")}, {eMyMoney::Account::Type::Loan, QStringLiteral("loan")}, {eMyMoney::Account::Type::CertificateDep, QStringLiteral("certificatedep")}, {eMyMoney::Account::Type::Investment, QStringLiteral("investment")}, {eMyMoney::Account::Type::MoneyMarket, QStringLiteral("moneymarket")}, {eMyMoney::Account::Type::Asset, QStringLiteral("asset")}, {eMyMoney::Account::Type::Liability, QStringLiteral("liability")}, {eMyMoney::Account::Type::Currency, QStringLiteral("currency")}, {eMyMoney::Account::Type::Income, QStringLiteral("income")}, {eMyMoney::Account::Type::Expense, QStringLiteral("expense")}, {eMyMoney::Account::Type::AssetLoan, QStringLiteral("assetloan")}, {eMyMoney::Account::Type::Stock, QStringLiteral("stock")}, {eMyMoney::Account::Type::Equity, QStringLiteral("equity")}, }; return lut; } QString accountTypeAttributeToString(eMyMoney::Account::Type type) { return accountTypeAttributeLUT().value(type); } eMyMoney::Account::Type stringToAccountTypeAttribute(const QString &text) { return accountTypeAttributeLUT().key(text, eMyMoney::Account::Type::Unknown); } eMyMoney::Report::ReportType rowTypeToReportType(eMyMoney::Report::RowType rowType) { static const QHash reportTypes { {eMyMoney::Report::RowType::NoRows, eMyMoney::Report::ReportType::NoReport}, {eMyMoney::Report::RowType::AssetLiability, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::ExpenseIncome, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::Category, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::TopCategory, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Account, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Tag, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Payee, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Month, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Week, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::EquityType, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::AccountType, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Institution, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Budget, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::BudgetActual, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::Schedule, eMyMoney::Report::ReportType::InfoTable}, {eMyMoney::Report::RowType::AccountInfo, eMyMoney::Report::ReportType::InfoTable}, {eMyMoney::Report::RowType::AccountLoanInfo, eMyMoney::Report::ReportType::InfoTable}, {eMyMoney::Report::RowType::AccountReconcile, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::CashFlow, eMyMoney::Report::ReportType::QueryTable}, }; return reportTypes.value(rowType, eMyMoney::Report::ReportType::Invalid); } QHash budgetLevelLUT() { static const QHash lut { {eMyMoney::Budget::Level::None, QStringLiteral("none")}, {eMyMoney::Budget::Level::Monthly, QStringLiteral("monthly")}, {eMyMoney::Budget::Level::MonthByMonth, QStringLiteral("monthbymonth")}, {eMyMoney::Budget::Level::Yearly, QStringLiteral("yearly")}, {eMyMoney::Budget::Level::Max, QStringLiteral("invalid")}, }; return lut; } QString budgetNames(eMyMoney::Budget::Level textID) { return budgetLevelLUT().value(textID); } eMyMoney::Budget::Level stringToBudgetLevel(const QString &text) { return budgetLevelLUT().key(text, eMyMoney::Budget::Level::Max); } QHash budgetLevelsLUT() { static const QHash lut { {eMyMoney::Budget::Level::None, QStringLiteral("none")}, {eMyMoney::Budget::Level::Monthly, QStringLiteral("monthly")}, {eMyMoney::Budget::Level::MonthByMonth, QStringLiteral("monthbymonth")}, {eMyMoney::Budget::Level::Yearly, QStringLiteral("yearly")}, {eMyMoney::Budget::Level::Max, QStringLiteral("invalid")}, }; return lut; } QString budgetLevels(eMyMoney::Budget::Level textID) { return budgetLevelsLUT().value(textID); } void writeBaseXML(const QString &id, QDomDocument &document, QDomElement &el) { Q_UNUSED(document); el.setAttribute(QStringLiteral("id"), id); } MyMoneyReport readReport(const QDomElement &node) { if (nodeName(Node::Report) != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not REPORT"); MyMoneyReport report(node.attribute(attributeName(Attribute::Report::ID))); // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any report ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // report types supported in this version, of course) // read report's internals QString type = node.attribute(attributeName(Attribute::Report::Type)); if (type.startsWith(QLatin1String("pivottable"))) report.setReportType(eMyMoney::Report::ReportType::PivotTable); else if (type.startsWith(QLatin1String("querytable"))) report.setReportType(eMyMoney::Report::ReportType::QueryTable); else if (type.startsWith(QLatin1String("infotable"))) report.setReportType(eMyMoney::Report::ReportType::InfoTable); else throw MYMONEYEXCEPTION_CSTRING("Unknown report type"); report.setGroup(node.attribute(attributeName(Attribute::Report::Group))); report.clearTransactionFilter(); // read date tab QString datelockstr = node.attribute(attributeName(Attribute::Report::DateLock), "userdefined"); // Handle the pivot 1.2/query 1.1 case where the values were saved as // numbers bool ok = false; eMyMoney::TransactionFilter::Date dateLock = static_cast(datelockstr.toUInt(&ok)); if (!ok) { dateLock = stringToDateLockAttribute(datelockstr); } report.setDateFilter(dateLock); // read general tab report.setName(node.attribute(attributeName(Attribute::Report::Name))); report.setComment(node.attribute(attributeName(Attribute::Report::Comment), "Extremely old report")); report.setConvertCurrency(node.attribute(attributeName(Attribute::Report::ConvertCurrency), "1").toUInt()); report.setFavorite(node.attribute(attributeName(Attribute::Report::Favorite), "0").toUInt()); report.setSkipZero(node.attribute(attributeName(Attribute::Report::SkipZero), "0").toUInt()); const auto rowTypeFromXML = stringToRowType(node.attribute(attributeName(Attribute::Report::RowType))); if (report.reportType() == eMyMoney::Report::ReportType::PivotTable) { // read report's internals report.setIncludingBudgetActuals(node.attribute(attributeName(Attribute::Report::IncludesActuals), "0").toUInt()); report.setIncludingForecast(node.attribute(attributeName(Attribute::Report::IncludesForecast), "0").toUInt()); report.setIncludingPrice(node.attribute(attributeName(Attribute::Report::IncludesPrice), "0").toUInt()); report.setIncludingAveragePrice(node.attribute(attributeName(Attribute::Report::IncludesAveragePrice), "0").toUInt()); report.setMixedTime(node.attribute(attributeName(Attribute::Report::MixedTime), "0").toUInt()); report.setInvestmentsOnly(node.attribute(attributeName(Attribute::Report::Investments), "0").toUInt()); // read rows/columns tab if (node.hasAttribute(attributeName(Attribute::Report::Budget))) report.setBudget(node.attribute(attributeName(Attribute::Report::Budget))); if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) report.setRowType(rowTypeFromXML); else report.setRowType(eMyMoney::Report::RowType::ExpenseIncome); if (node.hasAttribute(attributeName(Attribute::Report::ShowRowTotals))) report.setShowingRowTotals(node.attribute(attributeName(Attribute::Report::ShowRowTotals)).toUInt()); else if (report.rowType() == eMyMoney::Report::RowType::ExpenseIncome) // for backward compatibility report.setShowingRowTotals(true); report.setShowingColumnTotals(node.attribute(attributeName(Attribute::Report::ShowColumnTotals), "1").toUInt()); //check for reports with older settings which didn't have the detail attribute const auto detailLevelFromXML = stringToDetailLevel(node.attribute(attributeName(Attribute::Report::Detail))); if (detailLevelFromXML != eMyMoney::Report::DetailLevel::End) report.setDetailLevel(detailLevelFromXML); else report.setDetailLevel(eMyMoney::Report::DetailLevel::All); report.setIncludingMovingAverage(node.attribute(attributeName(Attribute::Report::IncludesMovingAverage), "0").toUInt()); if (report.isIncludingMovingAverage()) report.setMovingAverageDays(node.attribute(attributeName(Attribute::Report::MovingAverageDays), "1").toUInt()); report.setIncludingSchedules(node.attribute(attributeName(Attribute::Report::IncludesSchedules), "0").toUInt()); report.setIncludingTransfers(node.attribute(attributeName(Attribute::Report::IncludesTransfers), "0").toUInt()); report.setIncludingUnusedAccounts(node.attribute(attributeName(Attribute::Report::IncludesUnused), "0").toUInt()); report.setColumnsAreDays(node.attribute(attributeName(Attribute::Report::ColumnsAreDays), "0").toUInt()); // read chart tab const auto chartTypeFromXML = stringToChartType(node.attribute(attributeName(Attribute::Report::ChartType))); if (chartTypeFromXML != eMyMoney::Report::ChartType::End) report.setChartType(chartTypeFromXML); else report.setChartType(eMyMoney::Report::ChartType::None); report.setChartCHGridLines(node.attribute(attributeName(Attribute::Report::ChartCHGridLines), "1").toUInt()); report.setChartSVGridLines(node.attribute(attributeName(Attribute::Report::ChartSVGridLines), "1").toUInt()); report.setChartDataLabels(node.attribute(attributeName(Attribute::Report::ChartDataLabels), "1").toUInt()); report.setChartByDefault(node.attribute(attributeName(Attribute::Report::ChartByDefault), "0").toUInt()); report.setLogYAxis(node.attribute(attributeName(Attribute::Report::LogYAxis), "0").toUInt()); report.setChartLineWidth(node.attribute(attributeName(Attribute::Report::ChartLineWidth), QString(MyMoneyReport::lineWidth())).toUInt()); // read range tab const auto columnTypeFromXML = stringToColumnType(node.attribute(attributeName(Attribute::Report::ColumnType))); if (columnTypeFromXML != eMyMoney::Report::ColumnType::Invalid) report.setColumnType(columnTypeFromXML); else report.setColumnType(eMyMoney::Report::ColumnType::Months); const auto dataLockFromXML = stringToDataLockAttribute(node.attribute(attributeName(Attribute::Report::DataLock))); if (dataLockFromXML != eMyMoney::Report::DataLock::DataOptionCount) report.setDataFilter(dataLockFromXML); else report.setDataFilter(eMyMoney::Report::DataLock::Automatic); report.setDataRangeStart(node.attribute(attributeName(Attribute::Report::DataRangeStart), "0")); report.setDataRangeEnd(node.attribute(attributeName(Attribute::Report::DataRangeEnd), "0")); report.setDataMajorTick(node.attribute(attributeName(Attribute::Report::DataMajorTick), "0")); report.setDataMinorTick(node.attribute(attributeName(Attribute::Report::DataMinorTick), "0")); report.setYLabelsPrecision(node.attribute(attributeName(Attribute::Report::YLabelsPrecision), "2").toUInt()); } else if (report.reportType() == eMyMoney::Report::ReportType::QueryTable) { // read rows/columns tab if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) report.setRowType(rowTypeFromXML); else report.setRowType(eMyMoney::Report::RowType::Account); unsigned qc = 0; QStringList columns = node.attribute(attributeName(Attribute::Report::QueryColumns), "none").split(','); foreach (const auto column, columns) { const int queryColumnFromXML = stringToQueryColumn(column); if (queryColumnFromXML != eMyMoney::Report::QueryColumn::End) qc |= queryColumnFromXML; } report.setQueryColumns(static_cast(qc)); report.setTax(node.attribute(attributeName(Attribute::Report::Tax), "0").toUInt()); report.setInvestmentsOnly(node.attribute(attributeName(Attribute::Report::Investments), "0").toUInt()); report.setLoansOnly(node.attribute(attributeName(Attribute::Report::Loans), "0").toUInt()); report.setHideTransactions(node.attribute(attributeName(Attribute::Report::HideTransactions), "0").toUInt()); report.setShowingColumnTotals(node.attribute(attributeName(Attribute::Report::ShowColumnTotals), "1").toUInt()); const auto detailLevelFromXML = stringToDetailLevel(node.attribute(attributeName(Attribute::Report::Detail), "none")); if (detailLevelFromXML == eMyMoney::Report::DetailLevel::All) report.setDetailLevel(detailLevelFromXML); else report.setDetailLevel(eMyMoney::Report::DetailLevel::None); // read performance or capital gains tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::Performance) report.setInvestmentSum(static_cast(node.attribute(attributeName(Attribute::Report::InvestmentSum), QString::number(static_cast(eMyMoney::Report::InvestmentSum::Period))).toInt())); // read capital gains tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { report.setInvestmentSum(static_cast(node.attribute(attributeName(Attribute::Report::InvestmentSum), QString::number(static_cast(eMyMoney::Report::InvestmentSum::Sold))).toInt())); if (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold) { report.setShowSTLTCapitalGains(node.attribute(attributeName(Attribute::Report::ShowSTLTCapitalGains), "0").toUInt()); report.setSettlementPeriod(node.attribute(attributeName(Attribute::Report::SettlementPeriod), "3").toUInt()); report.setTermSeparator(QDate::fromString(node.attribute(attributeName(Attribute::Report::TermsSeparator), QDate::currentDate().addYears(-1).toString(Qt::ISODate)),Qt::ISODate)); } } } else if (report.reportType() == eMyMoney::Report::ReportType::InfoTable) { if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) report.setRowType(rowTypeFromXML); else report.setRowType(eMyMoney::Report::RowType::AccountInfo); if (node.hasAttribute(attributeName(Attribute::Report::ShowRowTotals))) report.setShowingRowTotals(node.attribute(attributeName(Attribute::Report::ShowRowTotals)).toUInt()); else report.setShowingRowTotals(true); } QDomNode child = node.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (elementName(Element::Report::Text) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Pattern))) { report.setTextFilter(QRegExp(c.attribute(attributeName(Attribute::Report::Pattern)), c.attribute(attributeName(Attribute::Report::CaseSensitive), "1").toUInt() ? Qt::CaseSensitive : Qt::CaseInsensitive, c.attribute(attributeName(Attribute::Report::RegEx), "1").toUInt() ? QRegExp::Wildcard : QRegExp::RegExp), c.attribute(attributeName(Attribute::Report::InvertText), "0").toUInt()); } if (elementName(Element::Report::Type) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Type))) { const auto reportType = stringToTypeAttribute(c.attribute(attributeName(Attribute::Report::Type))); if (reportType != -1) report.addType(reportType); } if (elementName(Element::Report::State) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::State))) { const auto state = stringToStateAttribute(c.attribute(attributeName(Attribute::Report::State))); if (state != -1) report.addState(state); } if (elementName(Element::Report::Validity) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Validity))) { const auto validity = stringToValidityAttribute(c.attribute(attributeName(Attribute::Report::Validity))); if (validity != -1) report.addValidity(validity); } if (elementName(Element::Report::Number) == c.tagName()) report.setNumberFilter(c.attribute(attributeName(Attribute::Report::From)), c.attribute(attributeName(Attribute::Report::To))); if (elementName(Element::Report::Amount) == c.tagName()) report.setAmountFilter(MyMoneyMoney(c.attribute(attributeName(Attribute::Report::From), "0/100")), MyMoneyMoney(c.attribute(attributeName(Attribute::Report::To), "0/100"))); if (elementName(Element::Report::Dates) == c.tagName()) { QDate from, to; if (c.hasAttribute(attributeName(Attribute::Report::From))) from = QDate::fromString(c.attribute(attributeName(Attribute::Report::From)), Qt::ISODate); if (c.hasAttribute(attributeName(Attribute::Report::To))) to = QDate::fromString(c.attribute(attributeName(Attribute::Report::To)), Qt::ISODate); report.setDateFilter(from, to); } if (elementName(Element::Report::Payee) == c.tagName()) report.addPayee(c.attribute(attributeName(Attribute::Report::ID))); if (elementName(Element::Report::Tag) == c.tagName()) report.addTag(c.attribute(attributeName(Attribute::Report::ID))); if (elementName(Element::Report::Category) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::ID))) report.addCategory(c.attribute(attributeName(Attribute::Report::ID))); if (elementName(Element::Report::Account) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::ID))) report.addAccount(c.attribute(attributeName(Attribute::Report::ID))); #if 0 // account groups had a severe problem in versions 5.0.0 to 5.0.2. Therefor, we don't read them // in anymore and rebuild them internally. They are written to the file nevertheless to maintain // compatibility to older versions which rely on them. I left the old code for reference here // ipwizard - 2019-01-13 if (elementName(Element::Report::AccountGroup) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Group))) { const auto groupType = stringToAccountTypeAttribute(c.attribute(attributeName(Attribute::Report::Group))); if (groupType != eMyMoney::Account::Type::Unknown) report.addAccountGroup(groupType); } #endif child = child.nextSibling(); } return report; } void writeReport(const MyMoneyReport &report, QDomDocument &document, QDomElement &parent) { auto el = document.createElement(nodeName(Node::Report)); // No matter what changes, be sure to have a 'type' attribute. Only change // the major type if it becomes impossible to maintain compatibility with // older versions of the program as new features are added to the reports. // Feel free to change the minor type every time a change is made here. // write report's internals if (report.reportType() == eMyMoney::Report::ReportType::PivotTable) el.setAttribute(attributeName(Attribute::Report::Type), "pivottable 1.15"); else if (report.reportType() == eMyMoney::Report::ReportType::QueryTable) el.setAttribute(attributeName(Attribute::Report::Type), "querytable 1.14"); else if (report.reportType() == eMyMoney::Report::ReportType::InfoTable) el.setAttribute(attributeName(Attribute::Report::Type), "infotable 1.0"); el.setAttribute(attributeName(Attribute::Report::Group), report.group()); el.setAttribute(attributeName(Attribute::Report::ID), report.id()); // write general tab - auto anonymous = false; - if (anonymous) { - el.setAttribute(attributeName(Attribute::Report::Name), report.id()); - el.setAttribute(attributeName(Attribute::Report::Comment), QString(report.comment()).fill('x')); - } else { - el.setAttribute(attributeName(Attribute::Report::Name), report.name()); - el.setAttribute(attributeName(Attribute::Report::Comment), report.comment()); - } + el.setAttribute(attributeName(Attribute::Report::Name), report.name()); + el.setAttribute(attributeName(Attribute::Report::Comment), report.comment()); + el.setAttribute(attributeName(Attribute::Report::ConvertCurrency), report.isConvertCurrency()); el.setAttribute(attributeName(Attribute::Report::Favorite), report.isFavorite()); el.setAttribute(attributeName(Attribute::Report::SkipZero), report.isSkippingZero()); el.setAttribute(attributeName(Attribute::Report::DateLock), dateLockAttributeToString(report.dateRange())); el.setAttribute(attributeName(Attribute::Report::RowType), reportNames(report.rowType())); if (report.reportType() == eMyMoney::Report::ReportType::PivotTable) { // write report's internals el.setAttribute(attributeName(Attribute::Report::IncludesActuals), report.isIncludingBudgetActuals()); el.setAttribute(attributeName(Attribute::Report::IncludesForecast), report.isIncludingForecast()); el.setAttribute(attributeName(Attribute::Report::IncludesPrice), report.isIncludingPrice()); el.setAttribute(attributeName(Attribute::Report::IncludesAveragePrice), report.isIncludingAveragePrice()); el.setAttribute(attributeName(Attribute::Report::MixedTime), report.isMixedTime()); el.setAttribute(attributeName(Attribute::Report::Investments), report.isInvestmentsOnly()); // it's setable in rows/columns tab of querytable, but here it is internal setting // write rows/columns tab if (!report.budget().isEmpty()) el.setAttribute(attributeName(Attribute::Report::Budget), report.budget()); el.setAttribute(attributeName(Attribute::Report::ShowRowTotals), report.isShowingRowTotals()); el.setAttribute(attributeName(Attribute::Report::ShowColumnTotals), report.isShowingColumnTotals()); el.setAttribute(attributeName(Attribute::Report::Detail), reportNames(report.detailLevel())); el.setAttribute(attributeName(Attribute::Report::IncludesMovingAverage), report.isIncludingMovingAverage()); if (report.isIncludingMovingAverage()) el.setAttribute(attributeName(Attribute::Report::MovingAverageDays), report.movingAverageDays()); el.setAttribute(attributeName(Attribute::Report::IncludesSchedules), report.isIncludingSchedules()); el.setAttribute(attributeName(Attribute::Report::IncludesTransfers), report.isIncludingTransfers()); el.setAttribute(attributeName(Attribute::Report::IncludesUnused), report.isIncludingUnusedAccounts()); el.setAttribute(attributeName(Attribute::Report::ColumnsAreDays), report.isColumnsAreDays()); el.setAttribute(attributeName(Attribute::Report::ChartType), reportNames(report.chartType())); el.setAttribute(attributeName(Attribute::Report::ChartCHGridLines), report.isChartCHGridLines()); el.setAttribute(attributeName(Attribute::Report::ChartSVGridLines), report.isChartSVGridLines()); el.setAttribute(attributeName(Attribute::Report::ChartDataLabels), report.isChartDataLabels()); el.setAttribute(attributeName(Attribute::Report::ChartByDefault), report.isChartByDefault()); el.setAttribute(attributeName(Attribute::Report::LogYAxis), report.isLogYAxis()); el.setAttribute(attributeName(Attribute::Report::ChartLineWidth), report.chartLineWidth()); el.setAttribute(attributeName(Attribute::Report::ColumnType), reportNames(report.columnType())); el.setAttribute(attributeName(Attribute::Report::DataLock), reportNames(report.dataFilter())); el.setAttribute(attributeName(Attribute::Report::DataRangeStart), report.dataRangeStart()); el.setAttribute(attributeName(Attribute::Report::DataRangeEnd), report.dataRangeEnd()); el.setAttribute(attributeName(Attribute::Report::DataMajorTick), report.dataMajorTick()); el.setAttribute(attributeName(Attribute::Report::DataMinorTick), report.dataMinorTick()); el.setAttribute(attributeName(Attribute::Report::YLabelsPrecision), report.yLabelsPrecision()); } else if (report.reportType() == eMyMoney::Report::ReportType::QueryTable) { // write rows/columns tab QStringList columns; unsigned qc = report.queryColumns(); unsigned it_qc = eMyMoney::Report::QueryColumn::Begin; unsigned index = 1; while (it_qc != eMyMoney::Report::QueryColumn::End) { if (qc & it_qc) columns += reportNamesForQC(static_cast(it_qc)); it_qc *= 2; index++; } el.setAttribute(attributeName(Attribute::Report::QueryColumns), columns.join(",")); el.setAttribute(attributeName(Attribute::Report::Tax), report.isTax()); el.setAttribute(attributeName(Attribute::Report::Investments), report.isInvestmentsOnly()); el.setAttribute(attributeName(Attribute::Report::Loans), report.isLoansOnly()); el.setAttribute(attributeName(Attribute::Report::HideTransactions), report.isHideTransactions()); el.setAttribute(attributeName(Attribute::Report::ShowColumnTotals), report.isShowingColumnTotals()); el.setAttribute(attributeName(Attribute::Report::Detail), reportNames(report.detailLevel())); // write performance tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::Performance || report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) el.setAttribute(attributeName(Attribute::Report::InvestmentSum), static_cast(report.investmentSum())); // write capital gains tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { if (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold) { el.setAttribute(attributeName(Attribute::Report::SettlementPeriod), report.settlementPeriod()); el.setAttribute(attributeName(Attribute::Report::ShowSTLTCapitalGains), report.isShowingSTLTCapitalGains()); el.setAttribute(attributeName(Attribute::Report::TermsSeparator), report.termSeparator().toString(Qt::ISODate)); } } } else if (report.reportType() == eMyMoney::Report::ReportType::InfoTable) el.setAttribute(attributeName(Attribute::Report::ShowRowTotals), report.isShowingRowTotals()); // // Text Filter // QRegExp textfilter; if (report.textFilter(textfilter)) { QDomElement f = document.createElement(elementName(Element::Report::Text)); f.setAttribute(attributeName(Attribute::Report::Pattern), textfilter.pattern()); f.setAttribute(attributeName(Attribute::Report::CaseSensitive), (textfilter.caseSensitivity() == Qt::CaseSensitive) ? 1 : 0); f.setAttribute(attributeName(Attribute::Report::RegEx), (textfilter.patternSyntax() == QRegExp::Wildcard) ? 1 : 0); f.setAttribute(attributeName(Attribute::Report::InvertText), report.MyMoneyTransactionFilter::isInvertingText()); el.appendChild(f); } // // Type & State Filters // QList typelist; if (report.types(typelist) && ! typelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_type = typelist.constBegin(); while (it_type != typelist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Type)); p.setAttribute(attributeName(Attribute::Report::Type), typeAttributeToString(*it_type)); el.appendChild(p); ++it_type; } } QList statelist; if (report.states(statelist) && ! statelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_state = statelist.constBegin(); while (it_state != statelist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::State)); p.setAttribute(attributeName(Attribute::Report::State), stateAttributeToString(*it_state)); el.appendChild(p); ++it_state; } } QList validitylist; if (report.validities(validitylist) && ! validitylist.empty()) { // iterate over payees, and add each one QList::const_iterator it_validity = validitylist.constBegin(); while (it_validity != validitylist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Validity)); p.setAttribute(attributeName(Attribute::Report::Validity), validityAttributeToString(*it_validity)); el.appendChild(p); ++it_validity; } } // // Number Filter // QString nrFrom, nrTo; if (report.numberFilter(nrFrom, nrTo)) { QDomElement f = document.createElement(elementName(Element::Report::Number)); f.setAttribute(attributeName(Attribute::Report::From), nrFrom); f.setAttribute(attributeName(Attribute::Report::To), nrTo); el.appendChild(f); } // // Amount Filter // MyMoneyMoney from, to; if (report.amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); QDomElement f = document.createElement(elementName(Element::Report::Amount)); f.setAttribute(attributeName(Attribute::Report::From), from.toString()); f.setAttribute(attributeName(Attribute::Report::To), to.toString()); el.appendChild(f); } // // Payees Filter // QStringList payeelist; if (report.payees(payeelist)) { if (payeelist.empty()) { QDomElement p = document.createElement(elementName(Element::Report::Payee)); el.appendChild(p); } else { // iterate over payees, and add each one QStringList::const_iterator it_payee = payeelist.constBegin(); while (it_payee != payeelist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Payee)); p.setAttribute(attributeName(Attribute::Report::ID), *it_payee); el.appendChild(p); ++it_payee; } } } // // Tags Filter // QStringList taglist; if (report.tags(taglist)) { if (taglist.empty()) { QDomElement p = document.createElement(elementName(Element::Report::Tag)); el.appendChild(p); } else { // iterate over tags, and add each one QStringList::const_iterator it_tag = taglist.constBegin(); while (it_tag != taglist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Tag)); p.setAttribute(attributeName(Attribute::Report::ID), *it_tag); el.appendChild(p); ++it_tag; } } } // // Account Groups Filter // QList accountgrouplist; if (report.accountGroups(accountgrouplist)) { // iterate over accounts, and add each one QList::const_iterator it_group = accountgrouplist.constBegin(); while (it_group != accountgrouplist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::AccountGroup)); p.setAttribute(attributeName(Attribute::Report::Group), accountTypeAttributeToString(*it_group)); el.appendChild(p); ++it_group; } } // // Accounts Filter // QStringList accountlist; if (report.accounts(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Account)); p.setAttribute(attributeName(Attribute::Report::ID), *it_account); el.appendChild(p); ++it_account; } } // // Categories Filter // accountlist.clear(); if (report.categories(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Category)); p.setAttribute(attributeName(Attribute::Report::ID), *it_account); el.appendChild(p); ++it_account; } } // // Date Filter // if (report.dateRange() == eMyMoney::TransactionFilter::Date::UserDefined) { QDate dateFrom, dateTo; if (report.dateFilter(dateFrom, dateTo)) { QDomElement f = document.createElement(elementName(Element::Report::Dates)); if (dateFrom.isValid()) f.setAttribute(attributeName(Attribute::Report::From), dateFrom.toString(Qt::ISODate)); if (dateTo.isValid()) f.setAttribute(attributeName(Attribute::Report::To), dateTo.toString(Qt::ISODate)); el.appendChild(f); } } parent.appendChild(el); } MyMoneyBudget readBudget(const QDomElement &node) { if (nodeName(Node::Budget) != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not BUDGET"); MyMoneyBudget budget(node.attribute(QStringLiteral("id"))); // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any Budget ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // Budget types supported in this version, of course) budget.setName(node.attribute(attributeName(Attribute::Budget::Name))); budget.setBudgetStart(QDate::fromString(node.attribute(attributeName(Attribute::Budget::Start)), Qt::ISODate)); QDomNode child = node.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); MyMoneyBudget::AccountGroup account; if (elementName(Element::Budget::Account) == c.tagName()) { if (c.hasAttribute(attributeName(Attribute::Budget::ID))) account.setId(c.attribute(attributeName(Attribute::Budget::ID))); if (c.hasAttribute(attributeName(Attribute::Budget::BudgetLevel))) account.setBudgetLevel(stringToBudgetLevel(c.attribute(attributeName(Attribute::Budget::BudgetLevel)))); if (c.hasAttribute(attributeName(Attribute::Budget::BudgetSubAccounts))) account.setBudgetSubaccounts(c.attribute(attributeName(Attribute::Budget::BudgetSubAccounts)).toUInt()); } QDomNode period = c.firstChild(); while (!period.isNull() && period.isElement()) { QDomElement per = period.toElement(); MyMoneyBudget::PeriodGroup pGroup; if (elementName(Element::Budget::Period) == per.tagName() && per.hasAttribute(attributeName(Attribute::Budget::Amount)) && per.hasAttribute(attributeName(Attribute::Budget::Start))) { pGroup.setAmount(MyMoneyMoney(per.attribute(attributeName(Attribute::Budget::Amount)))); pGroup.setStartDate(QDate::fromString(per.attribute(attributeName(Attribute::Budget::Start)), Qt::ISODate)); account.addPeriod(pGroup.startDate(), pGroup); } period = period.nextSibling(); } budget.setAccount(account, account.id()); child = child.nextSibling(); } return budget; } const int BUDGET_VERSION = 2; void writeBudget(const MyMoneyBudget &budget, QDomDocument &document, QDomElement &parent) { auto el = document.createElement(nodeName(Node::Budget)); writeBaseXML(budget.id(), document, el); el.setAttribute(attributeName(Attribute::Budget::Name), budget.name()); el.setAttribute(attributeName(Attribute::Budget::Start), budget.budgetStart().toString(Qt::ISODate)); el.setAttribute(attributeName(Attribute::Budget::Version), BUDGET_VERSION); QMap::const_iterator it; auto accounts = budget.accountsMap(); for (it = accounts.cbegin(); it != accounts.cend(); ++it) { // only add the account if there is a budget entered // or it covers some sub accounts if (!(*it).balance().isZero() || (*it).budgetSubaccounts()) { QDomElement domAccount = document.createElement(elementName(Element::Budget::Account)); domAccount.setAttribute(attributeName(Attribute::Budget::ID), it.key()); domAccount.setAttribute(attributeName(Attribute::Budget::BudgetLevel), budgetLevels(it.value().budgetLevel())); domAccount.setAttribute(attributeName(Attribute::Budget::BudgetSubAccounts), it.value().budgetSubaccounts()); const QMap periods = it.value().getPeriods(); QMap::const_iterator it_per; for (it_per = periods.begin(); it_per != periods.end(); ++it_per) { if (!(*it_per).amount().isZero()) { QDomElement domPeriod = document.createElement(elementName(Element::Budget::Period)); domPeriod.setAttribute(attributeName(Attribute::Budget::Amount), (*it_per).amount().toString()); domPeriod.setAttribute(attributeName(Attribute::Budget::Start), (*it_per).startDate().toString(Qt::ISODate)); domAccount.appendChild(domPeriod); } } el.appendChild(domAccount); } } parent.appendChild(el); } } diff --git a/kmymoney/settings/kmymoneysettings_addons.cpp b/kmymoney/settings/kmymoneysettings_addons.cpp index 788b5029a..a3bbbe454 100644 --- a/kmymoney/settings/kmymoneysettings_addons.cpp +++ b/kmymoney/settings/kmymoneysettings_addons.cpp @@ -1,144 +1,143 @@ /*************************************************************************** kmymoneysettings_addons.cpp ------------------- copyright : (C) 2018 by Thomas Baumgart email : tbaumgart@kde.org ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes QFont KMyMoneySettings::listCellFontEx() { if (useSystemFont()) { return QFontDatabase::systemFont(QFontDatabase::GeneralFont); } else { return listCellFont(); } } QFont KMyMoneySettings::listHeaderFontEx() { if (useSystemFont()) { QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont); font.setBold(true); return font; } else { return listHeaderFont(); } } QColor KMyMoneySettings::schemeColor(const SchemeColor color) { switch(color) { case SchemeColor::ListBackground1: return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::NormalBackground).color(); case SchemeColor::ListBackground2: return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::AlternateBackground).color(); case SchemeColor::ListGrid: return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::InactiveText).color(); case SchemeColor::ListHighlightText: return KColorScheme (QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::NormalText).color(); case SchemeColor::ListHighlight: return KColorScheme (QPalette::Active, KColorScheme::Selection).background(KColorScheme::NormalBackground).color(); case SchemeColor::WindowText: return KColorScheme (QPalette::Active, KColorScheme::Window).foreground(KColorScheme::NormalText).color(); case SchemeColor::WindowBackground: return KColorScheme (QPalette::Active, KColorScheme::Window).background(KColorScheme::NormalBackground).color(); case SchemeColor::Positive: return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::PositiveText).color(); case SchemeColor::Negative: return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); case SchemeColor::TransactionImported: if (useCustomColors()) return transactionImportedColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::LinkBackground).color(); case SchemeColor::TransactionMatched: if (useCustomColors()) return transactionMatchedColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::LinkBackground).color(); case SchemeColor::TransactionErroneous: if (useCustomColors()) return transactionErroneousColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); case SchemeColor::FieldRequired: if (useCustomColors()) return fieldRequiredColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::NeutralBackground).color(); case SchemeColor::GroupMarker: if (useCustomColors()) return groupMarkerColor(); else return KColorScheme (QPalette::Active, KColorScheme::Selection).background(KColorScheme::LinkBackground).color(); case SchemeColor::MissingConversionRate: if (useCustomColors()) return missingConversionRateColor(); else return KColorScheme (QPalette::Active, KColorScheme::Complementary).foreground(KColorScheme::LinkText).color(); default: return QColor(); } } QStringList KMyMoneySettings::listOfItems() { bool prevValue = self()->useDefaults(true); QStringList all = itemList().split(',', QString::SkipEmptyParts); self()->useDefaults(prevValue); QStringList list = itemList().split(',', QString::SkipEmptyParts); // now add all from 'all' that are missing in 'list' QRegExp exp("-?(\\d+)"); QStringList::iterator it_s; for (it_s = all.begin(); it_s != all.end(); ++it_s) { - exp.indexIn(*it_s); - if (!list.contains(exp.cap(1)) && !list.contains(QString("-%1").arg(exp.cap(1)))) { + if ((exp.indexIn(*it_s) != -1) && !list.contains(exp.cap(1)) && !list.contains(QString("-%1").arg(exp.cap(1)))) { list << *it_s; } } return list; } int KMyMoneySettings::firstFiscalMonth() { return fiscalYearBegin() + 1; } int KMyMoneySettings::firstFiscalDay() { return fiscalYearBeginDay(); } QDate KMyMoneySettings::firstFiscalDate() { QDate date = QDate(QDate::currentDate().year(), firstFiscalMonth(), firstFiscalDay()); if (date > QDate::currentDate()) date = date.addYears(-1); return date; } diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index 45edce3ec..0e9084ed0 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,1855 +1,1856 @@ /*************************************************************************** khomeview_p.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KHOMEVIEW_P_H #define KHOMEVIEW_P_H #include "khomeview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "mymoneyutils.h" #include "kmymoneyutils.h" #include "kwelcomepage.h" #include "kmymoneysettings.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" #include "kmymoneyplugin.h" #include "mymoneyenums.h" #include "menuenums.h" #include "plugins/views/reports/reportsviewenums.h" #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" using namespace Icons; using namespace eMyMoney; /** * @brief Converts a QPixmap to an data URI scheme * * According to RFC 2397 * * @param pixmap Source to convert * @return full data URI */ QString QPixmapToDataUri(const QPixmap& pixmap) { QImage image(pixmap.toImage()); QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer return QLatin1String("data:image/png;base64,") + QString(byteArray.toBase64()); } bool accountNameLess(const MyMoneyAccount &acc1, const MyMoneyAccount &acc2) { return acc1.name().localeAwareCompare(acc2.name()) < 0; } class KHomeViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KHomeView) public: explicit KHomeViewPrivate(KHomeView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), m_view(nullptr), m_showAllSchedules(false), m_needLoad(true), m_netWorthGraphLastValidSize(400, 300), - m_currentPrinter(nullptr) + m_currentPrinter(nullptr), + m_scrollBarPos(0) { } ~KHomeViewPrivate() { // if user wants to remember the font size, store it here if (KMyMoneySettings::rememberZoomFactor() && m_view) { KMyMoneySettings::setZoomFactor(m_view->zoomFactor()); KMyMoneySettings::self()->save(); } } /** * Definition of bitmap used as argument for showAccounts(). */ enum paymentTypeE { Preferred = 1, ///< show preferred accounts Payment = 2 ///< show payment accounts }; void init() { Q_Q(KHomeView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); #ifdef ENABLE_WEBENGINE m_view = new QWebEngineView(q); #else m_view = new KWebView(q); #endif m_view->setPage(new MyQWebEnginePage(m_view)); vbox->addWidget(m_view); #ifdef ENABLE_WEBENGINE q->connect(m_view->page(), &QWebEnginePage::urlChanged, q, &KHomeView::slotOpenUrl); #else m_view->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); q->connect(m_view->page(), &KWebPage::linkClicked, q, &KHomeView::slotOpenUrl); #endif q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KHomeView::refresh); } /** * Print an account and its balance and limit */ void showAccountEntry(const MyMoneyAccount& acc, const MyMoneyMoney& value, const MyMoneyMoney& valueToMinBal, const bool showMinBal) { MyMoneyFile* file = MyMoneyFile::instance(); QString tmp; MyMoneySecurity currency = file->currency(acc.currencyId()); QString amount; QString amountToMinBal; //format amounts amount = MyMoneyUtils::formatMoney(value, acc, currency); amount.replace(QChar(' '), " "); if (showMinBal) { amountToMinBal = MyMoneyUtils::formatMoney(valueToMinBal, acc, currency); amountToMinBal.replace(QChar(' '), " "); } QString cellStatus, pathOK, pathTODO, pathNotOK; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { //show account's online-status pathOK = QPixmapToDataUri(Icons::get(Icon::DialogOKApply).pixmap(QSize(16,16))); pathTODO = QPixmapToDataUri(Icons::get(Icon::MailReceive).pixmap(QSize(16,16))); pathNotOK = QPixmapToDataUri(Icons::get(Icon::DialogCancel).pixmap(QSize(16,16))); if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) cellStatus = '-'; else if (file->hasMatchingOnlineBalance(acc)) { if (file->hasNewerTransaction(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate))) cellStatus = QString("").arg(pathTODO); else cellStatus = QString("").arg(pathOK); } else cellStatus = QString("").arg(pathNotOK); tmp = QString("%1").arg(cellStatus); } tmp += QString("") + link(VIEW_LEDGER, QString("?id=%1").arg(acc.id())) + acc.name() + linkend() + ""; int countNotMarked = 0, countCleared = 0, countNotReconciled = 0; QString countStr; if (KMyMoneySettings::showCountOfUnmarkedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotMarked = m_transactionStats[acc.id()][(int)Split::State::NotReconciled]; if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = m_transactionStats[acc.id()][(int)Split::State::Cleared]; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotReconciled = countNotMarked + countCleared; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) { if (countNotMarked) countStr = QString("%1").arg(countNotMarked); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfClearedTransactions()) { if (countCleared) countStr = QString("%1").arg(countCleared); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfNotReconciledTransactions()) { if (countNotReconciled) countStr = QString("%1").arg(countNotReconciled); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showDateOfLastReconciliation()) { const auto lastReconciliationDate = acc.lastReconciliationDate().toString(Qt::SystemLocaleShortDate).replace(QChar(' '), " "); tmp += QString("%1").arg(lastReconciliationDate); } //show account balance tmp += QString("%1").arg(showColoredAmount(amount, value.isNegative())); //show minimum balance column if requested if (showMinBal) { //if it is an investment, show minimum balance empty if (acc.accountType() == Account::Type::Investment) { tmp += QString(" "); } else { //show minimum balance entry tmp += QString("%1").arg(showColoredAmount(amountToMinBal, valueToMinBal.isNegative())); } } // qDebug("accountEntry = '%s'", tmp.toLatin1()); m_html += tmp; } void showAccountEntry(const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); MyMoneyMoney value; bool showLimit = KMyMoneySettings::showLimitInfo(); if (acc.accountType() == Account::Type::Investment) { //investment accounts show the balances of all its subaccounts value = investmentBalance(acc); //investment accounts have no minimum balance showAccountEntry(acc, value, MyMoneyMoney(), showLimit); } else { //get balance for normal accounts value = file->balance(acc.id(), QDate::currentDate()); if (acc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(acc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(file->baseCurrency().smallestAccountFraction()); m_total += baseValue; } else { m_total += value; } //if credit card or checkings account, show maximum credit if (acc.accountType() == Account::Type::CreditCard || acc.accountType() == Account::Type::Checkings) { QString maximumCredit = acc.value("maxCreditAbsolute"); if (maximumCredit.isEmpty()) { maximumCredit = acc.value("minBalanceAbsolute"); } MyMoneyMoney maxCredit = MyMoneyMoney(maximumCredit); showAccountEntry(acc, value, value - maxCredit, showLimit); } else { //otherwise use minimum balance QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); showAccountEntry(acc, value, value - minBalance, showLimit); } } } /** * @param acc the investment account * @return the balance in the currency of the investment account */ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto value = file->balance(acc.id(), QDate::currentDate()); foreach (const auto accountID, acc.accountList()) { auto stock = file->account(accountID); if (!stock.isClosed()) { try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id(), QDate::currentDate()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = (balance * price.rate(security.tradingCurrency())).convertPrecision(security.pricePrecision()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } } return value; } /** * Print text in the color set for negative numbers, if @p amount is negative * abd @p isNegative is true */ QString showColoredAmount(const QString& amount, bool isNegative) { if (isNegative) { //if negative, get the settings for negative numbers return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); } //if positive, return the same string return amount; } /** * Run the forecast */ void doForecast() { //clear m_accountList because forecast is about to changed m_accountList.clear(); //reinitialize the object m_forecast = KMyMoneyUtils::forecast(); //If forecastDays lower than accountsCycle, adjust to the first cycle if (m_forecast.accountsCycle() > m_forecast.forecastDays()) m_forecast.setForecastDays(m_forecast.accountsCycle()); //Get all accounts of the right type to calculate forecast m_forecast.doForecast(); } /** * Calculate the forecast balance after a payment has been made */ MyMoneyMoney forecastPaymentBalance(const MyMoneyAccount& acc, const MyMoneyMoney& payment, QDate& paymentDate) { //if paymentDate before or equal to currentDate set it to current date plus 1 //so we get to accumulate forecast balance correctly if (paymentDate <= QDate::currentDate()) paymentDate = QDate::currentDate().addDays(1); //check if the account is already there if (m_accountList.find(acc.id()) == m_accountList.end() || m_accountList[acc.id()].find(paymentDate) == m_accountList[acc.id()].end()) { if (paymentDate == QDate::currentDate()) { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate); } else { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate.addDays(-1)); } } m_accountList[acc.id()][paymentDate] = m_accountList[acc.id()][paymentDate] + payment; return m_accountList[acc.id()][paymentDate]; } void loadView() { Q_Q(KHomeView); m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; if (MyMoneyFile::instance()->storage()) { MyMoneyFile::instance()->accountList(list); } if (list.isEmpty()) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { // preload transaction statistics m_transactionStats = MyMoneyFile::instance()->countTransactionsWithSpecificReconciliationState(); // keep current location on page m_scrollBarPos = 0; #ifndef ENABLE_WEBENGINE m_scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); #endif //clear the forecast flag so it will be reloaded m_forecast.setForecastDone(false); const QString filename = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css"); QString header = QString("\n\n").arg(QUrl::fromLocalFile(filename).url()); header += KMyMoneyUtils::variableCSS(); header += "\n"; QString footer = "\n"; m_html.clear(); m_html += header; m_html += QString("
%1
").arg(i18n("Your Financial Summary")); QStringList settings = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; for (it = settings.constBegin(); it != settings.constEnd(); ++it) { int option = (*it).toInt(); if (option > 0) { switch (option) { case 1: // payments showPayments(); break; case 2: // preferred accounts showAccounts(Preferred, i18n("Preferred Accounts")); break; case 3: // payment accounts // Check if preferred accounts are shown separately if (settings.contains("2")) { showAccounts(static_cast(Payment | Preferred), i18n("Payment Accounts")); } else { showAccounts(Payment, i18n("Payment Accounts")); } break; case 4: // favorite reports showFavoriteReports(); break; case 5: // forecast showForecast(); break; case 6: // net worth graph over all accounts showNetWorthGraph(); break; case 8: // assets and liabilities showAssetsLiabilities(); break; case 9: // budget showBudget(); break; case 10: // cash flow summary showCashFlowSummary(); break; } m_html += "
 
\n"; } } m_html += "
"; m_html += link(VIEW_WELCOME, QString()) + i18n("Show KMyMoney welcome page") + linkend(); m_html += "
"; m_html += "
"; m_html += footer; m_view->setHtml(m_html, QUrl("file://")); #ifndef ENABLE_WEBENGINE if (m_scrollBarPos) { QMetaObject::invokeMethod(q, "slotAdjustScrollPos", Qt::QueuedConnection); } #endif } } void showNetWorthGraph() { Q_Q(KHomeView); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); m_netWorthGraphLastValidSize = netWorthGraphSize; m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); m_html += QString(""); m_html += QString(""); if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::NetWorthForecast); if (!variantReport.isNull()) { auto report = variantReport.value(); report->resize(m_netWorthGraphLastValidSize); m_html += QString("").arg(QPixmapToDataUri(report->grab())); delete report; } } else { m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); } m_html += QString(""); m_html += QString("
\"Networth\"
%1
"); } void showPayments() { MyMoneyFile* file = MyMoneyFile::instance(); QList overdues; QList schedule; int i = 0; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate::currentDate(), QDate::currentDate().addMonths(1), false); overdues = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedule.empty() && overdues.empty()) return; // HACK // Remove the finished schedules QList::Iterator d_it; //regular schedules d_it = schedule.begin(); while (d_it != schedule.end()) { if ((*d_it).isFinished()) { d_it = schedule.erase(d_it); continue; } ++d_it; } //overdue schedules d_it = overdues.begin(); while (d_it != overdues.end()) { if ((*d_it).isFinished()) { d_it = overdues.erase(d_it); continue; } ++d_it; } m_html += "
"; m_html += QString("
%1
\n").arg(i18n("Payments")); if (!overdues.isEmpty()) { m_html += "
 
\n"; qSort(overdues); QList::Iterator it; QList::Iterator it_f; m_html += ""; m_html += QString("\n").arg(showColoredAmount(i18n("Overdue payments"), true)); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (it = overdues.begin(); it != overdues.end(); ++it) { // determine number of overdue payments int cnt = (*it).transactionsRemainingUntil(QDate::currentDate().addDays(-1)); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it, cnt); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { qSort(schedule); // Extract todays payments if any QList todays; QList::Iterator t_it; for (t_it = schedule.begin(); t_it != schedule.end();) { if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { todays.append(*t_it); (*t_it).setNextDueDate((*t_it).nextPayment(QDate::currentDate())); // if adjustedNextDueDate is still currentDate then remove it from // scheduled payments if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { t_it = schedule.erase(t_it); continue; } } ++t_it; } if (todays.count() > 0) { m_html += "
 
\n"; m_html += ""; m_html += QString("\n").arg(i18n("Today's due payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (t_it = todays.begin(); t_it != todays.end(); ++t_it) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*t_it); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { m_html += "
 
\n"; QList::Iterator it; m_html += ""; m_html += QString("\n").arg(i18n("Future payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; // show all or the first 6 entries int cnt; cnt = (m_showAllSchedules) ? -1 : 6; bool needMoreLess = m_showAllSchedules; QDate lastDate = QDate::currentDate().addMonths(1); qSort(schedule); do { it = schedule.begin(); if (it == schedule.end()) break; // if the next due date is invalid (schedule is finished) // we remove it from the list QDate nextDate = (*it).nextDueDate(); if (!nextDate.isValid()) { schedule.erase(it); continue; } if (nextDate > lastDate) break; if (cnt == 0) { needMoreLess = true; break; } // in case we've shown the current recurrence as overdue, // we don't show it here again, but keep the schedule // as it might show up later in the list again if (!(*it).isOverdue()) { if (cnt > 0) --cnt; m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it); m_html += ""; // for single occurrence we have reported everything so we // better get out of here. if ((*it).occurrence() == Schedule::Occurrence::Once) { schedule.erase(it); continue; } } // if nextPayment returns an invalid date, setNextDueDate will // just skip it, resulting in a loop // we check the resulting date and erase the schedule if invalid if (!((*it).nextPayment((*it).nextDueDate())).isValid()) { schedule.erase(it); continue; } (*it).setNextDueDate((*it).nextPayment((*it).nextDueDate())); qSort(schedule); } while (1); if (needMoreLess) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += ""; m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; if (m_showAllSchedules) { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("reduced")) + i18nc("Less...", "Show fewer schedules on the list") + linkend(); } else { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("full")) + i18nc("More...", "Show more schedules on the list") + linkend(); } m_html += "
"; } } m_html += "
"; } void showPaymentEntry(const MyMoneySchedule& sched, int cnt = 1) { QString tmp; MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = sched.account(); if (!acc.id().isEmpty()) { MyMoneyTransaction t = sched.transaction(); // only show the entry, if it is still active if (!sched.isFinished()) { MyMoneySplit sp = t.splitByAccount(acc.id(), true); QString pathEnter = QPixmapToDataUri(Icons::get(Icon::KeyEnter).pixmap(QSize(16,16))); QString pathSkip = QPixmapToDataUri(Icons::get(Icon::MediaSkipForward).pixmap(QSize(16,16))); //show payment date tmp = QString("") + QLocale().toString(sched.adjustedNextDueDate(), QLocale::ShortFormat) + ""; if (!pathEnter.isEmpty()) tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=enter").arg(sched.id()), i18n("Enter schedule")) + QString("").arg(pathEnter) + linkend(); if (!pathSkip.isEmpty()) tmp += " " + link(VIEW_SCHEDULE, QString("?id=%1&mode=skip").arg(sched.id()), i18n("Skip schedule")) + QString("").arg(pathSkip) + linkend(); tmp += QString(" "); tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=edit").arg(sched.id()), i18n("Edit schedule")) + sched.name() + linkend(); //show quantity of payments overdue if any if (cnt > 1) tmp += i18np(" (%1 payment)", " (%1 payments)", cnt); //show account of the main split tmp += ""; tmp += QString(file->account(acc.id()).name()); //show amount of the schedule tmp += ""; const MyMoneySecurity& currency = MyMoneyFile::instance()->currency(acc.currencyId()); MyMoneyMoney payment = MyMoneyMoney(sp.value(t.commodity(), acc.currencyId()) * cnt); QString amount = MyMoneyUtils::formatMoney(payment, acc, currency); amount.replace(QChar(' '), " "); tmp += showColoredAmount(amount, payment.isNegative()); tmp += ""; //show balance after payments tmp += ""; QDate paymentDate = QDate(sched.adjustedNextDueDate()); MyMoneyMoney balanceAfter = forecastPaymentBalance(acc, payment, paymentDate); QString balance = MyMoneyUtils::formatMoney(balanceAfter, acc, currency); balance.replace(QChar(' '), " "); tmp += showColoredAmount(balance, balanceAfter.isNegative()); tmp += ""; // qDebug("paymentEntry = '%s'", tmp.toLatin1()); m_html += tmp; } } } catch (const MyMoneyException &e) { qDebug("Unable to display schedule entry: %s", e.what()); } } void showAccounts(paymentTypeE type, const QString& header) { MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QList accounts; auto showClosedAccounts = KMyMoneySettings::showAllAccounts(); // get list of all accounts file->accountList(accounts); for (QList::Iterator it = accounts.begin(); it != accounts.end();) { bool removeAccount = false; if (!(*it).isClosed() || showClosedAccounts) { switch ((*it).accountType()) { case Account::Type::Expense: case Account::Type::Income: // never show a category account // Note: This might be different in a future version when // the homepage also shows category based information removeAccount = true; break; // Asset and Liability accounts are only shown if they // have the preferred flag set case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Investment: // if preferred accounts are requested, then keep in list if ((*it).value("PreferredAccount") != "Yes" || (type & Preferred) == 0) { removeAccount = true; } break; // Check payment accounts. If payment and preferred is selected, // then always show them. If only payment is selected, then // show only if preferred flag is not set. case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: switch (type & (Payment | Preferred)) { case Payment: if ((*it).value("PreferredAccount") == "Yes") removeAccount = true; break; case Preferred: if ((*it).value("PreferredAccount") != "Yes") removeAccount = true; break; case Payment | Preferred: break; default: removeAccount = true; break; } break; // filter all accounts that are not used on homepage views default: removeAccount = true; break; } } else if ((*it).isClosed() || (*it).isInvest()) { // don't show if closed or a stock account removeAccount = true; } if (removeAccount) it = accounts.erase(it); else ++it; } if (!accounts.isEmpty()) { // sort the accounts by name qStableSort(accounts.begin(), accounts.end(), accountNameLess); QString tmp; int i = 0; tmp = "
" + header + "
\n
 
\n"; m_html += tmp; m_html += ""; m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::Download).pixmap(QSize(16,16))); m_html += QString("").arg(pathStatusHeader); } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += QString(""); if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += QString("").arg(i18n("Last Reconciled")); m_html += ""; //only show limit info if user chose to do so if (KMyMoneySettings::showLimitInfo()) { m_html += ""; } m_html += ""; m_total = 0; QList::const_iterator it_m; for (it_m = accounts.constBegin(); it_m != accounts.constEnd(); ++it_m) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showAccountEntry(*it_m); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); QString amount = m_total.formatMoney(file->baseCurrency().tradingSymbol(), prec); if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) m_html += ""; m_html += QString("").arg(i18n("Total")); if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += QString("").arg(showColoredAmount(amount, m_total.isNegative())); m_html += "
"; m_html += i18n("Account"); m_html += "!MC!R%1"; m_html += i18n("Current Balance"); m_html += ""; m_html += i18n("To Minimum Balance / Maximum Credit"); m_html += "
%1%1
"; } } void showFavoriteReports() { QList reports = MyMoneyFile::instance()->reportList(); if (!reports.isEmpty()) { bool firstTime = 1; int row = 0; QList::const_iterator it_report = reports.constBegin(); while (it_report != reports.constEnd()) { if ((*it_report).isFavorite()) { if (firstTime) { m_html += QString("
%1
\n
 
\n").arg(i18n("Favorite Reports")); m_html += ""; m_html += ""; firstTime = false; } m_html += QString("") .arg(row++ & 0x01 ? "even" : "odd") .arg(link(VIEW_REPORTS, QString("?id=%1").arg((*it_report).id()))) .arg((*it_report).name()) .arg(linkend()) .arg((*it_report).comment()); } ++it_report; } if (!firstTime) m_html += "
"; m_html += i18n("Report"); m_html += ""; m_html += i18n("Comment"); m_html += "
%2%3%4%5
"; } } void showForecast() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); accList = m_forecast.accountList(); if (accList.count() > 0) { // sort the accounts by name qStableSort(accList.begin(), accList.end(), accountNameLess); auto i = 0; auto colspan = 1; //get begin day auto beginDay = QDate::currentDate().daysTo(m_forecast.beginForecastDate()); //if begin day is today skip to next cycle if (beginDay == 0) beginDay = m_forecast.accountsCycle(); // Now output header m_html += QString("
%1
\n
 
\n").arg(i18n("%1 Day Forecast", m_forecast.forecastDays())); m_html += ""; m_html += ""; auto colWidth = 55 / (m_forecast.forecastDays() / m_forecast.accountsCycle()); for (i = 0; (i*m_forecast.accountsCycle() + beginDay) <= m_forecast.forecastDays(); ++i) { m_html += QString(""; colspan++; } m_html += ""; // Now output entries i = 0; QList::ConstIterator it_account; for (it_account = accList.constBegin(); it_account != accList.constEnd(); ++it_account) { //MyMoneyAccount acc = (*it_n); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString(""; qint64 dropZero = -1; //account dropped below zero qint64 dropMinimum = -1; //account dropped below minimum balance QString minimumBalance = (*it_account).value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); MyMoneySecurity currency; MyMoneyMoney forecastBalance; //change account to deep currency if account is an investment if ((*it_account).isInvest()) { MyMoneySecurity underSecurity = file->security((*it_account).currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security((*it_account).currencyId()); } for (auto f = beginDay; f <= m_forecast.forecastDays(); f += m_forecast.accountsCycle()) { forecastBalance = m_forecast.forecastBalance(*it_account, QDate::currentDate().addDays(f)); QString amount; amount = MyMoneyUtils::formatMoney(forecastBalance, *it_account, currency); amount.replace(QChar(' '), " "); m_html += QString("").arg(showColoredAmount(amount, forecastBalance.isNegative())); } m_html += ""; //Check if the account is going to be below zero or below the minimal balance in the forecast period //Check if the account is going to be below minimal balance dropMinimum = m_forecast.daysToMinimumBalance(*it_account); //Check if the account is going to be below zero in the future dropZero = m_forecast.daysToZeroBalance(*it_account); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = i18n("The balance of %1 is below the minimum balance %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; default: msg = i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18n("The balance of %1 is below %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } break; default: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } m_html += "
"; m_html += i18n("Account"); m_html += "").arg(colWidth); m_html += i18ncp("Forecast days", "%1 day", "%1 days", i * m_forecast.accountsCycle() + beginDay); m_html += "
") + link(VIEW_LEDGER, QString("?id=%1").arg((*it_account).id())) + (*it_account).name() + linkend() + "").arg(colWidth); m_html += QString("%1
%1
%1
"; } } QString link(const QString& view, const QString& query, const QString& _title = QString()) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString linkend() const { return QStringLiteral(""); } void showAssetsLiabilities() { QList accounts; QList::ConstIterator it; QList assets; QList liabilities; MyMoneyMoney netAssets; MyMoneyMoney netLiabilities; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int i = 0; // get list of all accounts file->accountList(accounts); for (it = accounts.constBegin(); it != accounts.constEnd();) { if (!(*it).isClosed()) { switch ((*it).accountType()) { // group all assets into one list but make sure that investment accounts always show up case Account::Type::Investment: assets << *it; break; case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Asset: case Account::Type::AssetLoan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { assets << *it; } break; // group the liabilities into the other case Account::Type::CreditCard: case Account::Type::Liability: case Account::Type::Loan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { liabilities << *it; } break; default: break; } } ++it; } //only do it if we have assets or liabilities account if (assets.count() > 0 || liabilities.count() > 0) { // sort the accounts by name qStableSort(assets.begin(), assets.end(), accountNameLess); qStableSort(liabilities.begin(), liabilities.end(), accountNameLess); QString statusHeader; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader; pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::ViewOutbox).pixmap(QSize(16,16))); statusHeader = QString("").arg(pathStatusHeader); } //print header m_html += "
" + i18n("Assets and Liabilities Summary") + "
\n
 
\n"; m_html += ""; //column titles m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; //intermediate row to separate both columns m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; QString placeHolder_Status, placeHolder_Counts; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) placeHolder_Status = ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) placeHolder_Counts = ""; if (KMyMoneySettings::showCountOfClearedTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) placeHolder_Counts += ""; //get asset and liability accounts QList::const_iterator asset_it = assets.constBegin(); QList::const_iterator liabilities_it = liabilities.constBegin(); for (; asset_it != assets.constEnd() || liabilities_it != liabilities.constEnd();) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //write an asset account if we still have any if (asset_it != assets.constEnd()) { MyMoneyMoney value; //investment accounts consolidate the balance of its subaccounts if ((*asset_it).accountType() == Account::Type::Investment) { value = investmentBalance(*asset_it); } else { value = MyMoneyFile::instance()->balance((*asset_it).id(), QDate::currentDate()); } //calculate balance for foreign currency accounts if ((*asset_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*asset_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netAssets += baseValue; } else { netAssets += value; } //show the account without minimum balance showAccountEntry(*asset_it, value, MyMoneyMoney(), false); ++asset_it; } else { //write a white space if we don't m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } //leave the intermediate column empty m_html += ""; //write a liability account if (liabilities_it != liabilities.constEnd()) { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*liabilities_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*liabilities_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*liabilities_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netLiabilities += baseValue; } else { netLiabilities += value; } //show the account without minimum balance showAccountEntry(*liabilities_it, value, MyMoneyMoney(), false); ++liabilities_it; } else { //leave the space empty if we run out of liabilities m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } m_html += ""; } //calculate net worth MyMoneyMoney netWorth = netAssets + netLiabilities; //format assets, liabilities and net worth QString amountAssets = netAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiabilities = netLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountNetWorth = netWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountAssets.replace(QChar(' '), " "); amountLiabilities.replace(QChar(' '), " "); amountNetWorth.replace(QChar(' '), " "); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //print total for assets m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Assets")).arg(placeHolder_Counts).arg(showColoredAmount(amountAssets, netAssets.isNegative())); //leave the intermediate column empty m_html += ""; //print total liabilities m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Liabilities")).arg(placeHolder_Counts).arg(showColoredAmount(amountLiabilities, netLiabilities.isNegative())); m_html += ""; //print net worth m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Net Worth")).arg(placeHolder_Counts).arg(showColoredAmount(amountNetWorth, netWorth.isNegative())); m_html += ""; m_html += "
"; m_html += statusHeader; m_html += ""; m_html += i18n("Asset Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += ""; m_html += statusHeader; m_html += ""; m_html += i18n("Liability Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += "
%2%4%2%4
%2%4
"; m_html += "
"; } } void showBudget() { m_html += "
" + i18n("Budget") + "
\n
 
\n"; m_html += ""; if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::Budget); if (!variantReport.isNull()) m_html.append(variantReport.toString()); } else { m_html += QString(""); m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); m_html += QString(""); } m_html += QString("
%1
"); } void showCashFlowSummary() { MyMoneyTransactionFilter filter; MyMoneyMoney incomeValue; MyMoneyMoney expenseValue; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); //set start and end of month dates QDate startOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1); QDate endOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().daysInMonth()); //Add total income and expenses for this month //get transactions for current month filter.setDateFilter(startOfMonth, endOfMonth); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if no transaction then skip and print total in zero if (transactions.size() > 0) { //get all transactions for this month foreach (const auto transaction, transactions) { //get the splits for each transaction foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto repSplitAcc = file->account(split.accountId()); //only add if it is an income or expense if (repSplitAcc.isIncomeExpense()) { MyMoneyMoney value; //convert to base currency if necessary if (repSplitAcc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curRate; value = value.convert(10000); } else { value = (split.shares() * MyMoneyMoney::MINUS_ONE); } //store depending on account type if (repSplitAcc.accountType() == Account::Type::Income) { incomeValue += value; } else { expenseValue += value; } } } } } } //format income and expenses QString amountIncome = incomeValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpense = expenseValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountIncome.replace(QChar(' '), " "); amountExpense.replace(QChar(' '), " "); //calculate schedules //Add all schedules for this month MyMoneyMoney scheduledIncome; MyMoneyMoney scheduledExpense; MyMoneyMoney scheduledLiquidTransfer; MyMoneyMoney scheduledOtherTransfer; //get overdues and schedules until the end of this month QList schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), endOfMonth, false); //Remove the finished schedules QList::Iterator finished_it; for (finished_it = schedule.begin(); finished_it != schedule.end();) { if ((*finished_it).isFinished()) { finished_it = schedule.erase(finished_it); continue; } ++finished_it; } //add income and expenses QList::Iterator sched_it; for (sched_it = schedule.begin(); sched_it != schedule.end();) { QDate nextDate = (*sched_it).nextDueDate(); int cnt = 0; while (nextDate.isValid() && nextDate <= endOfMonth) { ++cnt; nextDate = (*sched_it).nextPayment(nextDate); // for single occurrence nextDate will not change, so we // better get out of here. if ((*sched_it).occurrence() == Schedule::Occurrence::Once) break; } MyMoneyAccount acc = (*sched_it).account(); if (!acc.id().isEmpty()) { MyMoneyTransaction transaction = (*sched_it).transaction(); // only show the entry, if it is still active MyMoneySplit sp = transaction.splitByAccount(acc.id(), true); // take care of the autoCalc stuff if ((*sched_it).type() == Schedule::Type::LoanPayment) { nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); //make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, transaction.splits()) { acc = file->account(split.accountId()); // collect all overdues on the first day QDate schedDate = nextDate; if (QDate::currentDate() >= nextDate) schedDate = QDate::currentDate().addDays(1); balanceMap[acc.id()] += file->balance(acc.id(), QDate::currentDate()); } KMyMoneyUtils::calculateAutoLoan(*sched_it, transaction, balanceMap); } //go through the splits and assign to liquid or other transfers const QList splits = transaction.splits(); QList::const_iterator split_it; for (split_it = splits.constBegin(); split_it != splits.constEnd(); ++split_it) { if ((*split_it).accountId() != acc.id()) { auto repSplitAcc = file->account((*split_it).accountId()); //get the shares and multiply by the quantity of occurrences in the period MyMoneyMoney value = (*split_it).shares() * cnt; //convert to foreign currency if needed if (repSplitAcc.currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = value * curRate; value = value.convert(10000); } if ((repSplitAcc.isLiquidLiability() || repSplitAcc.isLiquidAsset()) && acc.accountGroup() != repSplitAcc.accountGroup()) { scheduledLiquidTransfer += value; } else if (repSplitAcc.isAssetLiability() && !repSplitAcc.isLiquidLiability() && !repSplitAcc.isLiquidAsset()) { scheduledOtherTransfer += value; } else if (repSplitAcc.isIncomeExpense()) { //income and expenses are stored as negative values if (repSplitAcc.accountType() == Account::Type::Income) scheduledIncome -= value; if (repSplitAcc.accountType() == Account::Type::Expense) scheduledExpense -= value; } } } } ++sched_it; } //format the currency strings QString amountScheduledIncome = scheduledIncome.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledExpense = scheduledExpense.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledLiquidTransfer = scheduledLiquidTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledOtherTransfer = scheduledOtherTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountScheduledIncome.replace(QChar(' '), " "); amountScheduledExpense.replace(QChar(' '), " "); amountScheduledLiquidTransfer.replace(QChar(' '), " "); amountScheduledOtherTransfer.replace(QChar(' '), " "); //get liquid assets and liabilities QList accounts; QList::const_iterator account_it; MyMoneyMoney liquidAssets; MyMoneyMoney liquidLiabilities; // get list of all accounts file->accountList(accounts); for (account_it = accounts.constBegin(); account_it != accounts.constEnd();) { if (!(*account_it).isClosed()) { switch ((*account_it).accountType()) { //group all assets into one list case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: { MyMoneyMoney value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance for foreign currency accounts if ((*account_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; liquidAssets += baseValue; liquidAssets = liquidAssets.convert(10000); } else { liquidAssets += value; } break; } //group the liabilities into the other case Account::Type::CreditCard: { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*account_it).currencyId() != file->baseCurrency().id()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; liquidLiabilities += baseValue; liquidLiabilities = liquidLiabilities.convert(10000); } else { liquidLiabilities += value; } break; } default: break; } } ++account_it; } //calculate net worth MyMoneyMoney liquidWorth = liquidAssets + liquidLiabilities; //format assets, liabilities and net worth QString amountLiquidAssets = liquidAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidLiabilities = liquidLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidWorth = liquidWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountLiquidAssets.replace(QChar(' '), " "); amountLiquidLiabilities.replace(QChar(' '), " "); amountLiquidWorth.replace(QChar(' '), " "); //show the summary m_html += "
" + i18n("Cash Flow Summary") + "
\n
 
\n"; //print header m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current income m_html += QString("").arg(showColoredAmount(amountIncome, incomeValue.isNegative())); //print the scheduled income m_html += QString("").arg(showColoredAmount(amountScheduledIncome, scheduledIncome.isNegative())); //print current expenses m_html += QString("").arg(showColoredAmount(amountExpense, expenseValue.isNegative())); //print the scheduled expenses m_html += QString("").arg(showColoredAmount(amountScheduledExpense, scheduledExpense.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Income and Expenses of Current Month"); m_html += "
"; m_html += i18n("Income"); m_html += ""; m_html += i18n("Scheduled Income"); m_html += ""; m_html += i18n("Expenses"); m_html += ""; m_html += i18n("Scheduled Expenses"); m_html += "
%2%2%2%2
"; //print header of assets and liabilities m_html += "
 
\n"; m_html += ""; //assets and liabilities title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current liquid assets m_html += QString("").arg(showColoredAmount(amountLiquidAssets, liquidAssets.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledLiquidTransfer, scheduledLiquidTransfer.isNegative())); //print current liabilities m_html += QString("").arg(showColoredAmount(amountLiquidLiabilities, liquidLiabilities.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledOtherTransfer, scheduledOtherTransfer.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Liquid Assets and Liabilities"); m_html += "
"; m_html += i18n("Liquid Assets"); m_html += ""; m_html += i18n("Transfers to Liquid Liabilities"); m_html += ""; m_html += i18n("Liquid Liabilities"); m_html += ""; m_html += i18n("Other Transfers"); m_html += "
%2%2%2%2
"; //final conclusion MyMoneyMoney profitValue = incomeValue + expenseValue + scheduledIncome + scheduledExpense; MyMoneyMoney expectedAsset = liquidAssets + scheduledIncome + scheduledExpense + scheduledLiquidTransfer + scheduledOtherTransfer; MyMoneyMoney expectedLiabilities = liquidLiabilities + scheduledLiquidTransfer; QString amountExpectedAsset = expectedAsset.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpectedLiabilities = expectedLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountProfit = profitValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountProfit.replace(QChar(' '), " "); amountExpectedAsset.replace(QChar(' '), " "); amountExpectedLiabilities.replace(QChar(' '), " "); //print header of cash flow status m_html += "
 
\n"; m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); m_html += ""; //print expected assets m_html += QString("").arg(showColoredAmount(amountExpectedAsset, expectedAsset.isNegative())); //print expected liabilities m_html += QString("").arg(showColoredAmount(amountExpectedLiabilities, expectedLiabilities.isNegative())); //print expected profit m_html += QString("").arg(showColoredAmount(amountProfit, profitValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Cash Flow Status"); m_html += "
 "; m_html += i18n("Expected Liquid Assets"); m_html += ""; m_html += i18n("Expected Liquid Liabilities"); m_html += ""; m_html += i18n("Expected Profit/Loss"); m_html += "
 %2%2%2
"; m_html += "
"; } KHomeView *q_ptr; /** * daily balances of an account */ typedef QMap dailyBalances; #ifdef ENABLE_WEBENGINE QWebEngineView *m_view; #else KWebView *m_view; #endif QString m_html; bool m_showAllSchedules; bool m_needLoad; MyMoneyForecast m_forecast; MyMoneyMoney m_total; /** * Hold the last valid size of the net worth graph * for the times when the needed size can't be computed. */ QSize m_netWorthGraphLastValidSize; QMap< QString, QVector > m_transactionStats; /** * daily forecast balance of accounts */ QMap m_accountList; QPrinter *m_currentPrinter; int m_scrollBarPos; }; #endif diff --git a/kmymoney/views/newspliteditor.cpp b/kmymoney/views/newspliteditor.cpp index e8f4412f2..6ba378f4b 100644 --- a/kmymoney/views/newspliteditor.cpp +++ b/kmymoney/views/newspliteditor.cpp @@ -1,391 +1,389 @@ /*************************************************************************** newspliteditor.cpp ------------------- begin : Sat Apr 9 2016 copyright : (C) 2016 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 "newspliteditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "ui_newspliteditor.h" #include "widgethintframe.h" #include "ledgerview.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; struct NewSplitEditor::Private { Private(NewSplitEditor* parent) : ui(new Ui_NewSplitEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , splitModel(0) , accepted(false) , costCenterRequired(false) - , costCenterOk(false) , showValuesInverted(false) , amountHelper(nullptr) { accountsModel->setObjectName("AccountNamesFilterProxyModel"); costCenterModel->setObjectName("SortedCostCenterModel"); statusModel.setObjectName("StatusModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } ~Private() { delete ui; } void createStatusEntry(eMyMoney::Split::State status); bool checkForValidSplit(bool doUserInteraction = true); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool amountChanged(CreditDebitHelper* valueHelper); Ui_NewSplitEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; SplitModel* splitModel; bool accepted; bool costCenterRequired; - bool costCenterOk; bool showValuesInverted; QStandardItemModel statusModel; QString transactionSplitId; MyMoneyAccount counterAccount; MyMoneyAccount category; CreditDebitHelper* amountHelper; }; void NewSplitEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } bool NewSplitEditor::Private::checkForValidSplit(bool doUserInteraction) { QStringList infos; bool rc = true; if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos } return rc; } bool NewSplitEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } } return rc; } bool NewSplitEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty()) { try { QModelIndex index = Models::instance()->accountsModel()->accountById(accountId); category = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); ui->numberEdit->setDisabled(isIncomeExpense); ui->numberLabel->setDisabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewSplitEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { if(model->data(index, (int)eLedgerModel::Role::AccountId) == ui->accountCombo->getSelected() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewSplitEditor::Private::amountChanged(CreditDebitHelper* valueHelper) { Q_UNUSED(valueHelper); bool rc = true; return rc; } NewSplitEditor::NewSplitEditor(QWidget* parent, const QString& counterAccountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { SplitView* view = qobject_cast(parent->parentWidget()); Q_ASSERT(view != 0); d->splitModel = qobject_cast(view->model()); QModelIndex index = Models::instance()->accountsModel()->accountById(counterAccountId); d->counterAccount = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK)); d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); auto const model = Models::instance()->accountsModel(); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort((int)eAccountsModel::Column::Account); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(amountChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); } NewSplitEditor::~NewSplitEditor() { } void NewSplitEditor::setShowValuesInverted(bool inverse) { d->showValuesInverted = inverse; } bool NewSplitEditor::showValuesInverted() { return d->showValuesInverted; } bool NewSplitEditor::accepted() const { return d->accepted; } void NewSplitEditor::acceptEdit() { if(d->checkForValidSplit()) { d->accepted = true; emit done(); } } void NewSplitEditor::reject() { emit done(); } void NewSplitEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } QString NewSplitEditor::accountId() const { return d->ui->accountCombo->getSelected(); } void NewSplitEditor::setAccountId(const QString& id) { d->ui->accountCombo->clearEditText(); d->ui->accountCombo->setSelected(id); } QString NewSplitEditor::memo() const { return d->ui->memoEdit->toPlainText(); } void NewSplitEditor::setMemo(const QString& memo) { d->ui->memoEdit->setPlainText(memo); } MyMoneyMoney NewSplitEditor::amount() const { return d->amountHelper->value(); } void NewSplitEditor::setAmount(MyMoneyMoney value) { d->amountHelper->setValue(value); } QString NewSplitEditor::costCenterId() const { const int row = d->ui->costCenterCombo->currentIndex(); QModelIndex index = d->ui->costCenterCombo->model()->index(row, 0); return d->ui->costCenterCombo->model()->data(index, CostCenterModel::CostCenterIdRole).toString(); } void NewSplitEditor::setCostCenterId(const QString& id) { QModelIndex index = Models::indexById(d->costCenterModel, CostCenterModel::CostCenterIdRole, id); if(index.isValid()) { d->ui->costCenterCombo->setCurrentIndex(index.row()); } } QString NewSplitEditor::number() const { return d->ui->numberEdit->text(); } void NewSplitEditor::setNumber(const QString& number) { d->ui->numberEdit->setText(number); } QString NewSplitEditor::splitId() const { return d->transactionSplitId; } void NewSplitEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewSplitEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewSplitEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewSplitEditor::amountChanged() { // d->amountChanged(d->amountHelper); // useless call as reported by coverity scan } diff --git a/kmymoney/views/newtransactioneditor.cpp b/kmymoney/views/newtransactioneditor.cpp index ab499b83c..ae3dd6e99 100644 --- a/kmymoney/views/newtransactioneditor.cpp +++ b/kmymoney/views/newtransactioneditor.cpp @@ -1,728 +1,727 @@ /*************************************************************************** newtransactioneditor.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 "newtransactioneditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "payeesmodel.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "ui_newtransactioneditor.h" #include "splitdialog.h" #include "widgethintframe.h" #include "icons/icons.h" #include "modelenums.h" #include "mymoneyenums.h" using namespace Icons; Q_GLOBAL_STATIC(QDate, lastUsedPostDate) class NewTransactionEditor::Private { public: Private(NewTransactionEditor* parent) : ui(new Ui_NewTransactionEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , payeesModel(new QSortFilterProxyModel(parent)) , accepted(false) , costCenterRequired(false) , amountHelper(nullptr) { accountsModel->setObjectName("NewTransactionEditor::accountsModel"); costCenterModel->setObjectName("SortedCostCenterModel"); payeesModel->setObjectName("SortedPayeesModel"); statusModel.setObjectName("StatusModel"); splitModel.setObjectName("SplitModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); payeesModel->setSortLocaleAware(true); payeesModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } ~Private() { delete ui; } void createStatusEntry(eMyMoney::Split::State status); void updateWidgetState(); bool checkForValidTransaction(bool doUserInteraction = true); bool isDatePostOpeningDate(const QDate& date, const QString& accountId); bool postdateChanged(const QDate& date); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool valueChanged(CreditDebitHelper* valueHelper); Ui_NewTransactionEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; QSortFilterProxyModel* payeesModel; bool accepted; bool costCenterRequired; - bool costCenterOk; SplitModel splitModel; QStandardItemModel statusModel; QString transactionSplitId; MyMoneyAccount m_account; MyMoneyTransaction transaction; MyMoneySplit split; CreditDebitHelper* amountHelper; }; void NewTransactionEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } void NewTransactionEditor::Private::updateWidgetState() { // just in case it is disabled we turn it on ui->costCenterCombo->setEnabled(true); // setup the category/account combo box. If we have a split transaction, we disable the // combo box altogether. Changes can only be made via the split dialog editor bool blocked = false; QModelIndex index; // update the category combo box ui->accountCombo->setEnabled(true); switch(splitModel.rowCount()) { case 0: ui->accountCombo->setSelected(QString()); break; case 1: index = splitModel.index(0, 0); ui->accountCombo->setSelected(splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString()); break; default: index = splitModel.index(0, 0); blocked = ui->accountCombo->lineEdit()->blockSignals(true); ui->accountCombo->lineEdit()->setText(i18n("Split transaction")); ui->accountCombo->setDisabled(true); ui->accountCombo->lineEdit()->blockSignals(blocked); ui->costCenterCombo->setDisabled(true); ui->costCenterLabel->setDisabled(true); break; } ui->accountCombo->hidePopup(); // update the costcenter combo box if(ui->costCenterCombo->isEnabled()) { // extract the cost center index = splitModel.index(0, 0); QModelIndexList ccList = costCenterModel->match(costCenterModel->index(0, 0), CostCenterModel::CostCenterIdRole, splitModel.data(index, (int)eLedgerModel::Role::CostCenterId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (ccList.count() > 0) { index = ccList.front(); ui->costCenterCombo->setCurrentIndex(index.row()); } } } bool NewTransactionEditor::Private::checkForValidTransaction(bool doUserInteraction) { QStringList infos; bool rc = true; if(!postdateChanged(ui->dateEdit->date())) { infos << ui->dateEdit->toolTip(); rc = false; } if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos about the problem } return rc; } bool NewTransactionEditor::Private::isDatePostOpeningDate(const QDate& date, const QString& accountId) { bool rc = true; try { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = account.isIncomeExpense(); // we don't check for categories if(!isIncomeExpense) { if(date < account.openingDate()) rc = false; } } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } return rc; } bool NewTransactionEditor::Private::postdateChanged(const QDate& date) { bool rc = true; WidgetHintFrame::hide(ui->dateEdit, i18n("The posting date of the transaction.")); // collect all account ids QStringList accountIds; accountIds << m_account.id(); for(int row = 0; row < splitModel.rowCount(); ++row) { QModelIndex index = splitModel.index(row, 0); accountIds << splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString();; } Q_FOREACH(QString accountId, accountIds) { if(!isDatePostOpeningDate(date, accountId)) { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is prior to the opening date of account %1.", account.name())); rc = false; break; } } return rc; } bool NewTransactionEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } if(rc == true && splitModel.rowCount() == 1) { QModelIndex index = costCenterModel->index(costCenterIndex, 0); QString costCenterId = costCenterModel->data(index, CostCenterModel::CostCenterIdRole).toString(); index = splitModel.index(0, 0); splitModel.setData(index, costCenterId, (int)eLedgerModel::Role::CostCenterId); } } return rc; } bool NewTransactionEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty() && splitModel.rowCount() <= 1) { try { MyMoneyAccount category = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); rc &= postdateChanged(ui->dateEdit->date()); // make sure we have a split in the model bool newSplit = false; if(splitModel.rowCount() == 0) { splitModel.addEmptySplitEntry(); newSplit = true; } const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, accountId, (int)eLedgerModel::Role::AccountId); if(newSplit) { costCenterChanged(ui->costCenterCombo->currentIndex()); if(amountHelper->haveValue()) { splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to convert initial value to shares according to price information splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } } /// @todo we need to make sure to support multiple currencies here } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewTransactionEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { if(model->data(index, (int)eLedgerModel::Role::AccountId) == m_account.id() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewTransactionEditor::Private::valueChanged(CreditDebitHelper* valueHelper) { bool rc = true; if(valueHelper->haveValue() && splitModel.rowCount() <= 1) { rc = false; try { MyMoneyMoney shares; if(splitModel.rowCount() == 1) { const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to support multiple currencies splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } else { /// @todo ask what to do: if the rest of the splits is the same amount we could simply reverse the sign /// of all splits, otherwise we could ask if the user wants to start the split editor or anything else. } rc = true; } catch (MyMoneyException &e) { qDebug() << "Ooops: somwthing went wrong in" << Q_FUNC_INFO; } } return rc; } NewTransactionEditor::NewTransactionEditor(QWidget* parent, const QString& accountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { auto const model = Models::instance()->accountsModel(); // extract account information from model const auto index = model->accountById(accountId); d->m_account = model->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort(0); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); d->payeesModel->setSortRole(Qt::DisplayRole); d->payeesModel->setSourceModel(Models::instance()->payeesModel()); d->payeesModel->sort(0); d->ui->payeeEdit->setEditable(true); d->ui->payeeEdit->setModel(d->payeesModel); d->ui->payeeEdit->setModelColumn(0); d->ui->payeeEdit->completer()->setFilterMode(Qt::MatchContains); d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK)); d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); d->ui->statusCombo->setModel(&d->statusModel); d->ui->dateEdit->setDisplayFormat(QLocale().dateFormat(QLocale::ShortFormat)); d->ui->amountEditCredit->setAllowEmpty(true); d->ui->amountEditDebit->setAllowEmpty(true); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->dateEdit)); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->ui->dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(postdateChanged(QDate))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(valueChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); connect(d->ui->splitEditorButton, SIGNAL(clicked(bool)), this, SLOT(editSplits())); // handle some events in certain conditions different from default d->ui->payeeEdit->installEventFilter(this); d->ui->costCenterCombo->installEventFilter(this); d->ui->tagComboBox->installEventFilter(this); d->ui->statusCombo->installEventFilter(this); // setup tooltip // setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); } NewTransactionEditor::~NewTransactionEditor() { } bool NewTransactionEditor::accepted() const { return d->accepted; } void NewTransactionEditor::acceptEdit() { if(d->checkForValidTransaction()) { d->accepted = true; emit done(); } } void NewTransactionEditor::reject() { emit done(); } void NewTransactionEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } void NewTransactionEditor::loadTransaction(const QString& id) { const LedgerModel* model = Models::instance()->ledgerModel(); const QString transactionId = model->transactionIdFromTransactionSplitId(id); if(id.isEmpty()) { d->transactionSplitId.clear(); d->transaction = MyMoneyTransaction(); if(lastUsedPostDate()->isValid()) { d->ui->dateEdit->setDate(*lastUsedPostDate()); } else { d->ui->dateEdit->setDate(QDate::currentDate()); } bool blocked = d->ui->accountCombo->lineEdit()->blockSignals(true); d->ui->accountCombo->lineEdit()->clear(); d->ui->accountCombo->lineEdit()->blockSignals(blocked); } else { // find which item has this id and set is as the current item QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::TransactionId, QVariant(transactionId), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); Q_FOREACH(QModelIndex index, list) { // the selected split? const QString transactionSplitId = model->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString(); if(transactionSplitId == id) { d->transactionSplitId = id; d->transaction = model->data(index, (int)eLedgerModel::Role::Transaction).value(); d->split = model->data(index, (int)eLedgerModel::Role::Split).value(); d->ui->dateEdit->setDate(model->data(index, (int)eLedgerModel::Role::PostDate).toDate()); d->ui->payeeEdit->lineEdit()->setText(model->data(index, (int)eLedgerModel::Role::PayeeName).toString()); d->ui->memoEdit->clear(); d->ui->memoEdit->insertPlainText(model->data(index, (int)eLedgerModel::Role::Memo).toString()); d->ui->memoEdit->moveCursor(QTextCursor::Start); d->ui->memoEdit->ensureCursorVisible(); // The calculator for the amount field can simply be added as an icon to the line edit widget. // See http://stackoverflow.com/questions/11381865/how-to-make-an-extra-icon-in-qlineedit-like-this howto do it d->ui->amountEditCredit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Payment)).toString()); d->ui->amountEditDebit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Deposit)).toString()); d->ui->numberEdit->setText(model->data(index, (int)eLedgerModel::Role::Number).toString()); d->ui->statusCombo->setCurrentIndex(model->data(index, (int)eLedgerModel::Role::Number).toInt()); QModelIndexList stList = d->statusModel.match(d->statusModel.index(0, 0), Qt::UserRole+1, model->data(index, (int)eLedgerModel::Role::Reconciliation).toInt()); if(stList.count()) { QModelIndex stIndex = stList.front(); d->ui->statusCombo->setCurrentIndex(stIndex.row()); } } else { d->splitModel.addSplit(transactionSplitId); } } d->updateWidgetState(); } // set focus to date edit once we return to event loop QMetaObject::invokeMethod(d->ui->dateEdit, "setFocus", Qt::QueuedConnection); } void NewTransactionEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewTransactionEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewTransactionEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewTransactionEditor::postdateChanged(const QDate& date) { d->postdateChanged(date); } void NewTransactionEditor::valueChanged() { d->valueChanged(d->amountHelper); } void NewTransactionEditor::editSplits() { SplitModel splitModel; splitModel.deepCopy(d->splitModel, true); // create an empty split at the end splitModel.addEmptySplitEntry(); QPointer splitDialog = new SplitDialog(d->m_account, transactionAmount(), this); splitDialog->setModel(&splitModel); int rc = splitDialog->exec(); if(splitDialog && (rc == QDialog::Accepted)) { // remove that empty split again before we update the splits splitModel.removeEmptySplitEntry(); // copy the splits model contents d->splitModel.deepCopy(splitModel, true); // update the transaction amount d->amountHelper->setValue(splitDialog->transactionAmount()); d->updateWidgetState(); QWidget *next = d->ui->tagComboBox; if(d->ui->costCenterCombo->isEnabled()) { next = d->ui->costCenterCombo; } next->setFocus(); } if(splitDialog) { splitDialog->deleteLater(); } } MyMoneyMoney NewTransactionEditor::transactionAmount() const { return d->amountHelper->value(); } void NewTransactionEditor::saveTransaction() { MyMoneyTransaction t; if(!d->transactionSplitId.isEmpty()) { t = d->transaction; } else { // we keep the date when adding a new transaction // for the next new one *lastUsedPostDate() = d->ui->dateEdit->date(); } // first remove the splits that are gone foreach (const auto split, t.splits()) { if(split.id() == d->split.id()) { continue; } int row; for(row = 0; row < d->splitModel.rowCount(); ++row) { QModelIndex index = d->splitModel.index(row, 0); if(d->splitModel.data(index, (int)eLedgerModel::Role::SplitId).toString() == split.id()) { break; } } // if the split is not in the model, we get rid of it if(d->splitModel.rowCount() == row) { t.removeSplit(split); } } MyMoneyFileTransaction ft; try { // new we update the split we are opened for MyMoneySplit sp(d->split); sp.setNumber(d->ui->numberEdit->text()); sp.setMemo(d->ui->memoEdit->toPlainText()); sp.setShares(d->amountHelper->value()); if(t.commodity().isEmpty()) { t.setCommodity(d->m_account.currencyId()); sp.setValue(d->amountHelper->value()); } else { /// @todo check that the transactions commodity is the same /// as the one of the account this split references. If /// that is not the case, the next statement would create /// a problem sp.setValue(d->amountHelper->value()); } if(sp.reconcileFlag() != eMyMoney::Split::State::Reconciled && !sp.reconcileDate().isValid() && d->ui->statusCombo->currentIndex() == (int)eMyMoney::Split::State::Reconciled) { sp.setReconcileDate(QDate::currentDate()); } sp.setReconcileFlag(static_cast(d->ui->statusCombo->currentIndex())); // sp.setPayeeId(d->ui->payeeEdit->cu) if(sp.id().isEmpty()) { t.addSplit(sp); } else { t.modifySplit(sp); } t.setPostDate(d->ui->dateEdit->date()); // now update and add what we have in the model const SplitModel * model = &d->splitModel; for(int row = 0; row < model->rowCount(); ++row) { QModelIndex index = model->index(row, 0); MyMoneySplit s; const QString splitId = model->data(index, (int)eLedgerModel::Role::SplitId).toString(); if(!SplitModel::isNewSplitId(splitId)) { s = t.splitById(splitId); } s.setNumber(model->data(index, (int)eLedgerModel::Role::Number).toString()); s.setMemo(model->data(index, (int)eLedgerModel::Role::Memo).toString()); s.setAccountId(model->data(index, (int)eLedgerModel::Role::AccountId).toString()); s.setShares(model->data(index, (int)eLedgerModel::Role::SplitShares).value()); s.setValue(model->data(index, (int)eLedgerModel::Role::SplitValue).value()); s.setCostCenterId(model->data(index, (int)eLedgerModel::Role::CostCenterId).toString()); s.setPayeeId(model->data(index, (int)eLedgerModel::Role::PayeeId).toString()); // reconcile flag and date if(s.id().isEmpty()) { t.addSplit(s); } else { t.modifySplit(s); } } if(t.id().isEmpty()) { MyMoneyFile::instance()->addTransaction(t); } else { MyMoneyFile::instance()->modifyTransaction(t); } ft.commit(); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << "something went wrong" << e.what(); } } bool NewTransactionEditor::eventFilter(QObject* o, QEvent* e) { auto cb = qobject_cast(o); if (o) { // filter out wheel events for combo boxes if the popup view is not visible if ((e->type() == QEvent::Wheel) && !cb->view()->isVisible()) { return true; } } return QFrame::eventFilter(o, e); } // kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/kmymoney/views/widgethintframe.cpp b/kmymoney/views/widgethintframe.cpp index e9bbfc977..3e82a8eb4 100644 --- a/kmymoney/views/widgethintframe.cpp +++ b/kmymoney/views/widgethintframe.cpp @@ -1,231 +1,236 @@ /* * Copyright 2015-2018 Thomas Baumgart * * 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 "widgethintframe.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class WidgetHintFrameCollection::Private { public: QList widgetList; QList frameList; }; WidgetHintFrameCollection::WidgetHintFrameCollection(QObject* parent) : QObject(parent) , d(new Private) { } +WidgetHintFrameCollection::~WidgetHintFrameCollection() +{ + delete d; +} + void WidgetHintFrameCollection::addFrame(WidgetHintFrame* frame) { if(!d->frameList.contains(frame)) { connect(frame, &QObject::destroyed, this, &WidgetHintFrameCollection::frameDestroyed); connect(frame, &WidgetHintFrame::changed, this, [=] { QMetaObject::invokeMethod(this, "updateWidgets", Qt::QueuedConnection); }); d->frameList.append(frame); } } void WidgetHintFrameCollection::addWidget(QWidget* w) { if(!d->widgetList.contains(w)) { d->widgetList.append(w); updateWidgets(); } } void WidgetHintFrameCollection::removeWidget(QWidget* w) { d->widgetList.removeAll(w); w->setEnabled(true); } void WidgetHintFrameCollection::frameDestroyed(QObject* o) { WidgetHintFrame* frame = qobject_cast< WidgetHintFrame* >(o); if(frame) { d->frameList.removeAll(frame); } } void WidgetHintFrameCollection::updateWidgets() { bool enabled = true; Q_FOREACH(WidgetHintFrame* frame, d->frameList) { enabled &= !frame->isErroneous(); if(!enabled) { break; } } Q_FOREACH(QWidget* w, d->widgetList) { w->setEnabled(enabled); } } class WidgetHintFrame::Private { public: QWidget* editWidget; bool status; FrameStyle style; }; WidgetHintFrame::WidgetHintFrame(QWidget* editWidget, FrameStyle style, Qt::WindowFlags f) : QFrame(editWidget->parentWidget(), f) , d(new Private) { d->editWidget = 0; d->status = false; d->style = style; switch(style) { case Error: setStyleSheet("QFrame { background-color: none; padding: 1px; border: 2px solid red; border-radius: 4px; }"); break; case Warning: case Info: setStyleSheet("QFrame { background-color: none; padding: 1px; border: 2px dashed red; border-radius: 4px; }"); break; } attachToWidget(editWidget); } WidgetHintFrame::~WidgetHintFrame() { delete d; } bool WidgetHintFrame::isErroneous() const { return (d->style == Error) && (d->status == true); } static WidgetHintFrame* frame(QWidget* editWidget) { QList allErrorFrames = editWidget->parentWidget()->findChildren(); QList::const_iterator it; foreach(WidgetHintFrame* f, allErrorFrames) { if(f->editWidget() == editWidget) { return f; } } return 0; } void WidgetHintFrame::show(QWidget* editWidget, const QString& tooltip) { WidgetHintFrame* f = frame(editWidget); if(f) { f->QWidget::show(); f->d->status = true; emit f->changed(); } if(!tooltip.isNull()) editWidget->setToolTip(tooltip); } void WidgetHintFrame::hide(QWidget* editWidget, const QString& tooltip) { WidgetHintFrame* f = frame(editWidget); if(f) { f->QWidget::hide(); f->d->status = false; emit f->changed(); } if(!tooltip.isNull()) editWidget->setToolTip(tooltip); } QWidget* WidgetHintFrame::editWidget() const { return d->editWidget; } void WidgetHintFrame::detachFromWidget() { if(d->editWidget) { d->editWidget->removeEventFilter(this); d->editWidget = 0; } } void WidgetHintFrame::attachToWidget(QWidget* w) { // detach first detachFromWidget(); if(w) { d->editWidget = w; // make sure we receive changes in position and size w->installEventFilter(this); // place frame around widget move(w->pos() - QPoint(2, 2)); resize(w->width()+4, w->height()+4); // make sure widget is on top of frame w->raise(); // and hide frame for now QWidget::hide(); } } bool WidgetHintFrame::eventFilter(QObject* o, QEvent* e) { if(o == d->editWidget) { QMoveEvent* mev = 0; QResizeEvent* sev = 0; switch(e->type()) { case QEvent::EnabledChange: case QEvent::Hide: case QEvent::Show: /** * @todo think about what to do when widget is enabled/disabled * hidden or shown */ break; case QEvent::Move: mev = static_cast(e); move(mev->pos() - QPoint(2, 2)); break; case QEvent::Resize: sev = static_cast(e); resize(sev->size().width()+4, sev->size().height()+4); break; default: break; } } return QObject::eventFilter(o, e); } diff --git a/kmymoney/views/widgethintframe.h b/kmymoney/views/widgethintframe.h index 20f372a35..d273bf3cb 100644 --- a/kmymoney/views/widgethintframe.h +++ b/kmymoney/views/widgethintframe.h @@ -1,105 +1,106 @@ /* * Copyright 2015-2018 Thomas Baumgart * * 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 WIDGETHINTFRAME_H #define WIDGETHINTFRAME_H // ---------------------------------------------------------------------------- // QT Includes #include class QWidget; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class WidgetHintFrame : public QFrame { Q_OBJECT public: enum FrameStyle { Error = 0, Warning, Info }; Q_ENUM(FrameStyle) explicit WidgetHintFrame(QWidget* editWidget, FrameStyle style = Error, Qt::WindowFlags f = 0); ~WidgetHintFrame(); void attachToWidget(QWidget* w); void detachFromWidget(); bool isErroneous() const; QWidget* editWidget() const; /** * Shows the info frame around @a editWidget and in case @a tooltip * is not null (@sa QString::isNull()) the respective message will * be loaded into the @a editWidget's tooltip. In case @a tooltip is null * (the default) the @a editWidget's tooltip will not be changed. */ static void show(QWidget* editWidget, const QString& tooltip = QString()); /** * Hides the info frame around @a editWidget and in case @a tooltip * is not null (@sa QString::isNull()) the respective message will * be loaded into the @a editWidget's tooltip. In case @a tooltip is null * (the default) the @a editWidget's tooltip will not be changed. */ static void hide(QWidget* editWidget, const QString& tooltip = QString()); protected: bool eventFilter(QObject* o, QEvent* e) final override; Q_SIGNALS: void changed(); private: class Private; Private * const d; }; class WidgetHintFrameCollection : public QObject { Q_OBJECT public: explicit WidgetHintFrameCollection(QObject* parent = 0); + ~WidgetHintFrameCollection(); void addFrame(WidgetHintFrame* frame); void addWidget(QWidget* w); void removeWidget(QWidget* w); protected Q_SLOTS: virtual void frameDestroyed(QObject* o); virtual void updateWidgets(); Q_SIGNALS: void inputIsValid(bool valid); private: class Private; Private * const d; }; #endif // WIDGETHINTFRAME_H