diff --git a/kmymoney/models/payeesmodel.cpp b/kmymoney/models/payeesmodel.cpp index 5b85e2f98..92bb4876e 100644 --- a/kmymoney/models/payeesmodel.cpp +++ b/kmymoney/models/payeesmodel.cpp @@ -1,164 +1,172 @@ /* * Copyright 2016-2017 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 "payeesmodel.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneypayee.h" struct PayeesModel::Private { Private() {} QVector m_payeeItems; }; PayeesModel::PayeesModel(QObject* parent) : QAbstractListModel(parent) , d(new Private) { qDebug() << "Payees model created with items" << d->m_payeeItems.count(); d->m_payeeItems.clear(); } PayeesModel::~PayeesModel() { } int PayeesModel::rowCount(const QModelIndex& parent) const { // since the payees model is a simple table model, we only // return the rowCount for the hiddenRootItem. and zero otherwise if(parent.isValid()) { return 0; } return d->m_payeeItems.count(); } int PayeesModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return 1; } Qt::ItemFlags PayeesModel::flags(const QModelIndex& index) const { Qt::ItemFlags flags; if(!index.isValid()) return flags; if(index.row() < 0 || index.row() >= d->m_payeeItems.count()) return flags; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } QVariant PayeesModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case 0: return i18n("Payee"); break; } } return QAbstractItemModel::headerData(section, orientation, role); } QVariant PayeesModel::data(const QModelIndex& index, int role) const { if(!index.isValid()) return QVariant(); if(index.row() < 0 || index.row() >= d->m_payeeItems.count()) return QVariant(); QVariant rc; switch(role) { case Qt::DisplayRole: case Qt::EditRole: // make sure to never return any displayable text for the dummy entry if(!d->m_payeeItems[index.row()]->id().isEmpty()) { rc = d->m_payeeItems[index.row()]->name(); } else { rc = QString(); } break; case Qt::TextAlignmentRole: rc = QVariant(Qt::AlignLeft | Qt::AlignTop); break; case PayeeIdRole: rc = d->m_payeeItems[index.row()]->id(); break; } return rc; } bool PayeesModel::setData(const QModelIndex& index, const QVariant& value, int role) { if(!index.isValid()) { return false; } qDebug() << "setData(" << index.row() << index.column() << ")" << value << role; return QAbstractItemModel::setData(index, value, role); } void PayeesModel::unload() { if(rowCount() > 0) { beginRemoveRows(QModelIndex(), 0, rowCount() - 1); qDeleteAll(d->m_payeeItems); d->m_payeeItems.clear(); - QVector swp; - d->m_payeeItems.swap(swp); // changed behaviour from Qt 5.7 http://doc.qt.io/qt-5/qvector.html#clear + // From Qt 5.7, the capacity is preserved. To shed all capacity, + // swap with a default-constructed vector. + // see http://doc.qt.io/qt-5/qvector.html#clear + QVector().swap(d->m_payeeItems); endRemoveRows(); } } void PayeesModel::load() { const QList list = MyMoneyFile::instance()->payeeList(); - if(list.count() > 0) { - beginInsertRows(QModelIndex(), rowCount(), rowCount() + list.count()); + // first get rid of existing entries + unload(); + + const auto payeeCount = list.count(); + if(payeeCount > 0) { + // reserve some more slots than needed + d->m_payeeItems.reserve(payeeCount + 13); + beginInsertRows(QModelIndex(), rowCount(), rowCount() + payeeCount); // create an empty entry for those items that do not reference a payee d->m_payeeItems.append(new MyMoneyPayee()); foreach (const auto it, list) d->m_payeeItems.append(new MyMoneyPayee(it)); endInsertRows(); } } diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index fb24a0ab7..066dd1700 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3582 +1,3582 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2001-2002 Felix Rodriguez * Copyright 2002-2004 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006-2019 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyfile.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "mymoneybalancecache.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "onlinejob.h" #include "storageenums.h" #include "mymoneyenums.h" // include the following line to get a 'cout' for debug purposes // #include using namespace eMyMoney; const QString MyMoneyFile::AccountSeparator = QChar(':'); MyMoneyFile MyMoneyFile::file; typedef QList > BalanceNotifyList; typedef QMap CacheNotifyList; /// @todo make this template based class MyMoneyNotification { public: MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) : m_objType(File::Object::Transaction), m_notificationMode(mode), m_id(t.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) : m_objType(File::Object::Account), m_notificationMode(mode), m_id(acc.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) : m_objType(File::Object::Institution), m_notificationMode(mode), m_id(institution.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) : m_objType(File::Object::Payee), m_notificationMode(mode), m_id(payee.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) : m_objType(File::Object::Tag), m_notificationMode(mode), m_id(tag.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) : m_objType(File::Object::Schedule), m_notificationMode(mode), m_id(schedule.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) : m_objType(File::Object::Security), m_notificationMode(mode), m_id(security.id()) { } MyMoneyNotification(File::Mode mode, const onlineJob& job) : m_objType(File::Object::OnlineJob), m_notificationMode(mode), m_id(job.id()) { } File::Object objectType() const { return m_objType; } File::Mode notificationMode() const { return m_notificationMode; } const QString& id() const { return m_id; } protected: MyMoneyNotification(File::Object obj, File::Mode mode, const QString& id) : m_objType(obj), m_notificationMode(mode), m_id(id) {} private: File::Object m_objType; File::Mode m_notificationMode; QString m_id; }; class MyMoneyFile::Private { public: Private() : m_storage(0), m_inTransaction(false) {} ~Private() { delete m_storage; } /** * This method is used to add an id to the list of objects * to be removed from the cache. If id is empty, then nothing is added to the list. * * @param id id of object to be notified * @param reload reload the object (@c true) or not (@c false). The default is @c true * @see attach, detach */ void addCacheNotification(const QString& id, const QDate& date) { if (!id.isEmpty()) m_balanceNotifyList.append(std::make_pair(id, date)); } /** * This method is used to clear the notification list */ void clearCacheNotification() { // reset list to be empty m_balanceNotifyList.clear(); } /** * This method is used to clear all * objects mentioned in m_notificationList from the cache. */ void notify() { foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) { m_balanceChangedSet += i.first; if (i.second.isValid()) { m_balanceCache.clear(i.first, i.second); } else { m_balanceCache.clear(i.first); } } clearCacheNotification(); } /** * This method checks if a storage object is attached and * throws and exception if not. */ inline void checkStorage() const { if (m_storage == 0) throw MYMONEYEXCEPTION_CSTRING("No storage object attached to MyMoneyFile"); } /** * This method checks that a transaction has been started with * startTransaction() and throws an exception otherwise. Calls * checkStorage() to make sure a storage object is present and attached. */ void checkTransaction(const char* txt) const { checkStorage(); if (!m_inTransaction) throw MYMONEYEXCEPTION(QString::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(txt))); } void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) { // get all affected accounts and add them to the m_valueChangedSet QList accList; file.accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { QString currencyId = account_it->currencyId(); if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) { // this account is not in the base currency and the price affects it's value m_valueChangedSet.insert(account_it->id()); } } } /** * This member points to the storage strategy */ MyMoneyStorageMgr *m_storage; bool m_inTransaction; MyMoneySecurity m_baseCurrency; /** * @brief Cache for MyMoneyObjects * * It is also used to emit the objectAdded() and objectModified() signals. * => If one of these signals is used, you must use this cache. */ MyMoneyPriceList m_priceCache; MyMoneyBalanceCache m_balanceCache; /** * This member keeps a list of account ids to notify * after a single operation is completed. The balance cache * is cleared for that account and all dates on or after * the one supplied. If the date is invalid, the entire * balance cache is cleared for that account. */ BalanceNotifyList m_balanceNotifyList; /** * This member keeps a list of account ids for which * a balanceChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_balanceChangedSet; /** * This member keeps a list of account ids for which * a valueChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_valueChangedSet; /** * This member keeps the list of changes in the engine * in historical order. The type can be 'added', 'modified' * or removed. */ QList m_changeSet; }; class MyMoneyNotifier { public: MyMoneyNotifier(MyMoneyFile::Private* file) { m_file = file; m_file->clearCacheNotification(); } ~MyMoneyNotifier() { m_file->notify(); } private: MyMoneyFile::Private* m_file; }; MyMoneyFile::MyMoneyFile() : d(new Private) { } MyMoneyFile::~MyMoneyFile() { delete d; } MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) : d(new Private) { attachStorage(storage); } MyMoneyFile* MyMoneyFile::instance() { return &file; } void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage) { if (d->m_storage != 0) throw MYMONEYEXCEPTION_CSTRING("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION_CSTRING("Storage must not be 0"); d->m_storage = storage; // force reload of base currency d->m_baseCurrency = MyMoneySecurity(); // and the whole cache d->m_balanceCache.clear(); d->m_priceCache.clear(); // notify application about new data availability emit beginChangeNotification(); emit dataChanged(); emit endChangeNotification(); } void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */) { d->m_balanceCache.clear(); d->m_priceCache.clear(); d->m_storage = nullptr; } MyMoneyStorageMgr* MyMoneyFile::storage() const { return d->m_storage; } bool MyMoneyFile::storageAttached() const { return d->m_storage != 0; } void MyMoneyFile::startTransaction() { d->checkStorage(); if (d->m_inTransaction) { throw MYMONEYEXCEPTION_CSTRING("Already started a transaction!"); } d->m_storage->startTransaction(); d->m_inTransaction = true; d->m_changeSet.clear(); } bool MyMoneyFile::hasTransaction() const { return d->m_inTransaction; } void MyMoneyFile::commitTransaction() { d->checkTransaction(Q_FUNC_INFO); // commit the transaction in the storage const auto changed = d->m_storage->commitTransaction(); d->m_inTransaction = false; // collect notifications about removed objects QStringList removedObjects; const auto& set = d->m_changeSet; for (const auto& change : set) { switch (change.notificationMode()) { case File::Mode::Remove: removedObjects += change.id(); break; default: break; } } // inform the outside world about the beginning of notifications emit beginChangeNotification(); // Now it's time to send out some signals to the outside world // First we go through the d->m_changeSet and emit respective // signals about addition, modification and removal of engine objects const auto& changes = d->m_changeSet; for (const auto& change : changes) { switch (change.notificationMode()) { case File::Mode::Remove: emit objectRemoved(change.objectType(), change.id()); // if there is a balance change recorded for this account remove it since the account itself will be removed // this can happen when deleting categories that have transactions and the reassign category feature was used d->m_balanceChangedSet.remove(change.id()); break; case File::Mode::Add: if (!removedObjects.contains(change.id())) { emit objectAdded(change.objectType(), change.id()); } break; case File::Mode::Modify: if (!removedObjects.contains(change.id())) { emit objectModified(change.objectType(), change.id()); } break; } } // we're done with the change set, so we clear it d->m_changeSet.clear(); // now send out the balanceChanged signal for all those // accounts for which we have an indication about a possible // change. const auto& balanceChanges = d->m_balanceChangedSet; for (const auto& id : balanceChanges) { if (!removedObjects.contains(id)) { // if we notify about balance change we don't need to notify about value change // for the same account since a balance change implies a value change d->m_valueChangedSet.remove(id); emit balanceChanged(account(id)); } } d->m_balanceChangedSet.clear(); // now notify about the remaining value changes const auto& m_valueChanges = d->m_valueChangedSet; for (const auto& id : m_valueChanges) { if (!removedObjects.contains(id)) { emit valueChanged(account(id)); } } d->m_valueChangedSet.clear(); // as a last action, send out the global dataChanged signal if (changed) emit dataChanged(); // inform the outside world about the end of notifications emit endChangeNotification(); } void MyMoneyFile::rollbackTransaction() { d->checkTransaction(Q_FUNC_INFO); d->m_storage->rollbackTransaction(); d->m_inTransaction = false; d->m_balanceChangedSet.clear(); d->m_valueChangedSet.clear(); d->m_changeSet.clear(); } void MyMoneyFile::addInstitution(MyMoneyInstitution& institution) { // perform some checks to see that the institution stuff is OK. For // now we assume that the institution must have a name, the ID is not set // and it does not have a parent (MyMoneyFile). if (institution.name().length() == 0 || institution.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("Not a new institution"); d->checkTransaction(Q_FUNC_INFO); d->m_storage->addInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution); } void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction tCopy(transaction); // now check the splits bool loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account"); if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))) loanAccountAffected = true; } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); tCopy.modifySplit(s); } } } } // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current setting of this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list // and mark all accounts that are referenced const auto splits2 = tr.splits(); foreach (const auto& split, splits2) d->addCacheNotification(split.accountId(), tr.postDate()); // make sure the value is rounded to the accounts precision fixSplitPrecision(tCopy); // perform modification d->m_storage->modifyTransaction(tCopy); // and mark all accounts that are referenced const auto splits3 = tCopy.splits(); for (const auto& split : splits3) d->addCacheNotification(split.accountId(), tCopy.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction); } void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount account(_account); auto acc = MyMoneyFile::account(account.id()); // check that for standard accounts only specific parameters are changed if (isStandardAccount(account.id())) { // make sure to use the stuff we found on file account = acc; // and only use the changes that are allowed account.setName(_account.name()); account.setCurrencyId(_account.currencyId()); // now check that it is the same if (!(account == _account)) throw MYMONEYEXCEPTION_CSTRING("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION_CSTRING("Unable to change account type"); // if the account was moved to another institution, we notify // the old one as well as the new one and the structure change if (acc.institutionId() != account.institutionId()) { MyMoneyInstitution inst; if (!acc.institutionId().isEmpty()) { inst = institution(acc.institutionId()); inst.removeAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } if (!account.institutionId().isEmpty()) { inst = institution(account.institutionId()); inst.addAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } } d->m_storage->modifyAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account); } void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); // check that it's not one of the standard account groups if (isStandardAccount(acc.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent the standard account groups"); if (acc.accountGroup() == parent.accountGroup() || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense) || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) { if (acc.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent Stock to non-investment account"); if (parent.accountType() == Account::Type::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent non-stock to investment account"); // keep a notification of the current parent MyMoneyAccount curParent = account(acc.parentAccountId()); d->m_storage->reparentAccount(acc, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } else throw MYMONEYEXCEPTION_CSTRING("Unable to reparent to different account type"); } MyMoneyInstitution MyMoneyFile::institution(const QString& id) const { return d->m_storage->institution(id); } MyMoneyAccount MyMoneyFile::account(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id return MyMoneyAccount(); return d->m_storage->account(id); } MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const { static MyMoneyAccount nullAccount; const auto accounts = account.accountList(); for (const auto& acc : accounts) { const auto sacc = MyMoneyFile::account(acc); if (sacc.name().compare(name) == 0) return sacc; } return nullAccount; } MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const { try { return d->m_storage->accountByName(name); } catch (const MyMoneyException &) { } return MyMoneyAccount(); } void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the engine's idea about this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list const auto splits = tr.splits(); for (const auto& split : splits) { auto acc = account(split.accountId()); if (acc.isClosed()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove transaction that references a closed account.")); d->addCacheNotification(split.accountId(), tr.postDate()); //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ?? } d->m_storage->removeTransaction(transaction); // remove a possible notification of that same object from the changeSet QList::iterator it; for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) { if((*it).id() == transaction.id()) { it = d->m_changeSet.erase(it); } else { ++it; } } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction); } bool MyMoneyFile::hasActiveSplits(const QString& id) const { d->checkStorage(); return d->m_storage->hasActiveSplits(id); } bool MyMoneyFile::isStandardAccount(const QString& id) const { d->checkStorage(); return d->m_storage->isStandardAccount(id); } void MyMoneyFile::setAccountName(const QString& id, const QString& name) const { d->checkTransaction(Q_FUNC_INFO); auto acc = account(id); d->m_storage->setAccountName(id, name); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } void MyMoneyFile::removeAccount(const MyMoneyAccount& account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent; MyMoneyAccount acc; MyMoneyInstitution institution; // check that the account and its parent exist // this will throw an exception if the id is unknown acc = MyMoneyFile::account(account.id()); parent = MyMoneyFile::account(account.parentAccountId()); if (!acc.institutionId().isEmpty()) institution = MyMoneyFile::institution(acc.institutionId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits"); } // collect all sub-ordinate accounts for notification const auto accounts = acc.accountList(); for (const auto& id : accounts) d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id)); // don't forget the parent and a possible institution if (!institution.id().isEmpty()) { institution.removeAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } acc.setInstitutionId(QString()); d->m_storage->removeAccount(acc); d->m_balanceCache.clear(acc.id()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc); } void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::removeAccountList]!"); d->checkTransaction(Q_FUNC_INFO); // upon entry, we check that we could proceed with the operation if (!level) { if (!hasOnlyUnusedAccounts(account_list, 0)) { throw MYMONEYEXCEPTION_CSTRING("One or more accounts cannot be removed"); } } // process all accounts in the list and test if they have transactions assigned foreach (const auto sAccount, account_list) { auto a = d->m_storage->account(sAccount); //qDebug() << "Deleting account '"<< a.name() << "'"; // first remove all sub-accounts if (!a.accountList().isEmpty()) { removeAccountList(a.accountList(), level + 1); // then remove account itself, but we first have to get // rid of the account list that is still stored in // the MyMoneyAccount object. Easiest way is to get a fresh copy. a = d->m_storage->account(sAccount); } // make sure to remove the item from the cache removeAccount(a); } } bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!"); // process all accounts in the list and test if they have transactions assigned for (const auto& sAccount : account_list) { if (transactionCount(sAccount) != 0) return false; // the current account has a transaction assigned if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1)) return false; // some sub-account has a transaction assigned } return true; // all subaccounts unused } void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution inst = MyMoneyFile::institution(institution.id()); bool blocked = signalsBlocked(); blockSignals(true); const auto accounts = inst.accountList(); for (const auto& acc : accounts) { auto a = account(acc); a.setInstitutionId(QString()); modifyAccount(a); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a); } blockSignals(blocked); d->m_storage->removeInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution); } void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(baseCurrency().id()); MyMoneyFileTransaction ft; try { int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); } addAccount(newAccount, parentAccount); // in case of a loan account, we add the initial payment if ((newAccount.accountType() == Account::Type::Loan || newAccount.accountType() == Account::Type::AssetLoan) && !newAccount.value("kmm-loan-payment-acc").isEmpty() && !newAccount.value("kmm-loan-payment-date").isEmpty()) { MyMoneyAccountLoan acc(newAccount); MyMoneyTransaction t; MyMoneySplit a, b; a.setAccountId(acc.id()); b.setAccountId(acc.value("kmm-loan-payment-acc")); a.setValue(acc.loanAmount()); if (acc.accountType() == Account::Type::Loan) a.setValue(-a.value()); a.setShares(a.value()); b.setValue(-a.value()); b.setShares(b.value()); a.setMemo(i18n("Loan payout")); b.setMemo(i18n("Loan payout")); t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate)); newAccount.deletePair("kmm-loan-payment-acc"); newAccount.deletePair("kmm-loan-payment-date"); MyMoneyFile::instance()->modifyAccount(newAccount); t.addSplit(a); t.addSplit(b); addTransaction(t); createOpeningBalanceTransaction(newAccount, openingBal); // in case of an investment account we check if we should create // a brokerage account } else if (newAccount.accountType() == Account::Type::Investment && !brokerageAccount.name().isEmpty()) { addAccount(brokerageAccount, parentAccount); // set a link from the investment account to the brokerage account modifyAccount(newAccount); createOpeningBalanceTransaction(brokerageAccount, openingBal); } else createOpeningBalanceTransaction(newAccount, openingBal); ft.commit(); } catch (const MyMoneyException &e) { qWarning("Unable to create account: %s", e.what()); throw; } } void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution institution; // perform some checks to see that the account stuff is OK. For // now we assume that the account must have a name, has no // transaction and sub-accounts and parent account // it's own ID is not set and it does not have a pointer to (MyMoneyFile) if (account.name().length() == 0) throw MYMONEYEXCEPTION_CSTRING("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id"); if (account.accountType() == Account::Type::Unknown) throw MYMONEYEXCEPTION_CSTRING("Account has invalid type"); // make sure, that the parent account exists // if not, an exception is thrown. If it exists, // get a copy of the current data auto acc = MyMoneyFile::account(parent.id()); #if 0 // TODO: remove the following code as we now can have multiple accounts // with the same name even in the same hierarchy position of the account tree // // check if the selected name is currently not among the child accounts // if we find one, then return it as the new account QStringList::const_iterator it_a; foreach (const auto accountID, acc.accountList()) { MyMoneyAccount a = MyMoneyFile::account(accountID); if (account.name() == a.name()) { account = a; return; } } #endif // FIXME: make sure, that the parent has the same type // I left it out here because I don't know, if there is // a tight coupling between e.g. checking accounts and the // class asset. It certainly does not make sense to create an // expense account under an income account. Maybe it does, I don't know. // We enforce, that a stock account can never be a parent and // that the parent for a stock account must be an investment. Also, // an investment cannot have another investment account as it's parent if (parent.isInvest()) throw MYMONEYEXCEPTION_CSTRING("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Investment account can only have stock accounts as children"); // if an institution is set, verify that it exists if (account.institutionId().length() != 0) { // check the presence of the institution. if it // does not exist, an exception is thrown institution = MyMoneyFile::institution(account.institutionId()); } // if we don't have a valid opening date use today if (!account.openingDate().isValid()) { account.setOpeningDate(QDate::currentDate()); } // make sure to set the opening date for categories to a // fixed date (1900-1-1). See #313793 on b.k.o for details if (account.isIncomeExpense()) { account.setOpeningDate(QDate(1900, 1, 1)); } // if we don't have a currency assigned use the base currency if (account.currencyId().isEmpty()) { account.setCurrencyId(baseCurrency().id()); } // make sure the parent id is setup account.setParentAccountId(parent.id()); d->m_storage->addAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Add, account); d->m_storage->addAccount(parent, account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); if (account.institutionId().length() != 0) { institution.addAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } } MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance) { MyMoneyTransaction t; // if the opening balance is not zero, we need // to create the respective transaction if (!balance.isZero()) { d->checkTransaction(Q_FUNC_INFO); MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc = openingBalanceAccount(currency); if (openAcc.openingDate() > acc.openingDate()) { openAcc.setOpeningDate(acc.openingDate()); modifyAccount(openAcc); } MyMoneySplit s; t.setPostDate(acc.openingDate()); t.setCommodity(acc.currencyId()); s.setAccountId(acc.id()); s.setShares(balance); s.setValue(balance); t.addSplit(s); s.clearId(); s.setAccountId(openAcc.id()); s.setShares(-balance); s.setValue(-balance); t.addSplit(s); addTransaction(t); } return t; } QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const { QString result; MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc; try { openAcc = openingBalanceAccount(currency); } catch (const MyMoneyException &) { return result; } // Iterate over all the opening balance transactions for this currency MyMoneyTransactionFilter filter; filter.addAccount(openAcc.id()); QList transactions = transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account (*it_t).splitByAccount(acc.id(), true /*match*/); // If so, we have a winner! result = (*it_t).id(); break; } catch (const MyMoneyException &) { // If not, keep searching ++it_t; } } return result; } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) { if (!security.isCurrency()) throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported"); try { return openingBalanceAccount_internal(security); } catch (const MyMoneyException &) { MyMoneyFileTransaction ft; MyMoneyAccount acc; try { acc = createOpeningBalanceAccount(security); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to create opening balance account for security %s", qPrintable(security.id())); } return acc; } } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const { return openingBalanceAccount_internal(security); } MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const { if (!security.isCurrency()) throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported"); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && it->currencyId() == security.id()) { acc = *it; break; } } if (acc.id().isEmpty()) { for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) && it->currencyId() == security.id()) { acc = *it; break; } } } if (acc.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No opening balance account for %1").arg(security.tradingSymbol())); return acc; } MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); // find present opening balance accounts without containing '(' QString name; QString parentAccountId; QRegExp exp(QString("\\([A-Z]{3}\\)")); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && exp.indexIn(it->name()) == -1) { name = it->name(); parentAccountId = it->parentAccountId(); break; } } if (name.isEmpty()) name = MyMoneyFile::openingBalancesPrefix(); if (security.id() != baseCurrency().id()) { name += QString(" (%1)").arg(security.id()); } acc.setName(name); acc.setAccountType(Account::Type::Equity); acc.setCurrencyId(security.id()); acc.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity(); this->addAccount(acc, parent); return acc; } void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * no ids are assigned // * the date valid (must not be empty) // * the referenced accounts in the splits exist // first perform all the checks if (!transaction.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with invalid postdate"); // now check the splits auto loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account"); } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { foreach (const auto split, transaction.splits()) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); transaction.modifySplit(s); } } } } // check that we have a commodity if (transaction.commodity().isEmpty()) { transaction.setCommodity(baseCurrency().id()); } // make sure the value is rounded to the accounts precision fixSplitPrecision(transaction); // then add the transaction to the file global pool d->m_storage->addTransaction(transaction); // scan the splits again to update notification list const auto splits2 = transaction.splits(); for (const auto& split : splits2) d->addCacheNotification(split.accountId(), transaction.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction); } MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const { d->checkStorage(); return d->m_storage->transaction(id); } MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const { d->checkStorage(); return d->m_storage->transaction(account, idx); } void MyMoneyFile::addPayee(MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee); } MyMoneyPayee MyMoneyFile::payee(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return MyMoneyPayee(); return d->m_storage->payee(id); } MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const { d->checkStorage(); return d->m_storage->payeeByName(name); } void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee); } void MyMoneyFile::removePayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the payee is not referenced anymore d->m_storage->removePayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee); } void MyMoneyFile::addTag(MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag); } MyMoneyTag MyMoneyFile::tag(const QString& id) const { return d->m_storage->tag(id); } MyMoneyTag MyMoneyFile::tagByName(const QString& name) const { d->checkStorage(); return d->m_storage->tagByName(name); } void MyMoneyFile::modifyTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag); } void MyMoneyFile::removeTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the tag is not referenced anymore d->m_storage->removeTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag); } void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { d->checkStorage(); if (idlist.isEmpty()) { d->m_storage->accountList(list); #if 0 // TODO: I have no idea what this was good for, but it caused the networth report // to show double the numbers so I commented it out (ipwizard, 2008-05-24) if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) { d->m_storage->accountList(list); d->m_cache.preloadAccount(list); } #endif QList::Iterator it; for (it = list.begin(); it != list.end();) { if (isStandardAccount((*it).id())) { it = list.erase(it); } else { ++it; } } } else { QList::ConstIterator it; QList list_a; d->m_storage->accountList(list_a); for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) { if (!isStandardAccount((*it).id())) { if (idlist.indexOf((*it).id()) != -1) { list.append(*it); if (recursive == true && !(*it).accountList().isEmpty()) { accountList(list, (*it).accountList(), true); } } } } } } QList MyMoneyFile::institutionList() const { return d->m_storage->institutionList(); } // general get functions MyMoneyPayee MyMoneyFile::user() const { d->checkStorage(); return d->m_storage->user(); } // general set functions void MyMoneyFile::setUser(const MyMoneyPayee& user) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setUser(user); } bool MyMoneyFile::dirty() const { if (!d->m_storage) return false; return d->m_storage->dirty(); } void MyMoneyFile::setDirty() const { d->checkStorage(); d->m_storage->setDirty(); } unsigned int MyMoneyFile::accountCount() const { d->checkStorage(); return d->m_storage->accountCount(); } void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const { if (acc.currencyId().isEmpty()) { if (!baseCurrency().id().isEmpty()) acc.setCurrencyId(baseCurrency().id()); } } MyMoneyAccount MyMoneyFile::liability() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)); } MyMoneyAccount MyMoneyFile::asset() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)); } MyMoneyAccount MyMoneyFile::expense() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); } MyMoneyAccount MyMoneyFile::income() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)); } MyMoneyAccount MyMoneyFile::equity() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)); } unsigned int MyMoneyFile::transactionCount(const QString& account) const { d->checkStorage(); return d->m_storage->transactionCount(account); } unsigned int MyMoneyFile::transactionCount() const { return transactionCount(QString()); } QMap MyMoneyFile::transactionCountMap() const { d->checkStorage(); return d->m_storage->transactionCountMap(); } unsigned int MyMoneyFile::institutionCount() const { d->checkStorage(); return d->m_storage->institutionCount(); } MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const { if (date.isValid()) { MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date); if (bal.isValid()) return bal.balance(); } d->checkStorage(); MyMoneyMoney returnValue = d->m_storage->balance(id, date); if (date.isValid()) { d->m_balanceCache.insert(id, date, returnValue); } return returnValue; } MyMoneyMoney MyMoneyFile::balance(const QString& id) const { return balance(id, QDate()); } MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const { MyMoneyMoney cleared; QList list; cleared = balance(id, date); MyMoneyAccount account = this->account(id); MyMoneyMoney factor(1, 1); if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity) factor = -factor; MyMoneyTransactionFilter filter; filter.addAccount(id); filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); filter.addState((int)TransactionFilter::State::NotReconciled); transactionList(list, filter); for (QList::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); for (QList::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { const MyMoneySplit &split = (*it_s); if (split.accountId() != id) continue; cleared -= split.shares(); } } return cleared * factor; } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const { d->checkStorage(); return d->m_storage->totalBalance(id, date); } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const { return totalBalance(id, QDate()); } void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const { MyMoneySecurity from, to; try { from = security(fromId); to = security(toId); qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name())); } catch (const MyMoneyException &e) { qWarning("Missing security caught in MyMoneyFile::warningMissingRate(). %s", e.what()); } } void MyMoneyFile::transactionList(QList >& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } void MyMoneyFile::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } QList MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const { d->checkStorage(); return d->m_storage->transactionList(filter); } QList MyMoneyFile::payeeList() const { return d->m_storage->payeeList(); } QList MyMoneyFile::tagList() const { return d->m_storage->tagList(); } QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; QString rc; if (!accountId.isEmpty()) { acc = account(accountId); do { if (!rc.isEmpty()) rc = AccountSeparator + rc; rc = acc.name() + rc; acc = account(acc.parentAccountId()); } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id()))); } return rc; } QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const { QString id; // search the category in the expense accounts and if it is not found, try // to locate it in the income accounts if (type == Account::Type::Unknown || type == Account::Type::Expense) { id = locateSubAccount(MyMoneyFile::instance()->expense(), category); } if ((id.isEmpty() && type == Account::Type::Unknown) || type == Account::Type::Income) { id = locateSubAccount(MyMoneyFile::instance()->income(), category); } return id; } QString MyMoneyFile::categoryToAccount(const QString& category) const { return categoryToAccount(category, Account::Type::Unknown); } QString MyMoneyFile::nameToAccount(const QString& name) const { QString id; // search the category in the asset accounts and if it is not found, try // to locate it in the liability accounts id = locateSubAccount(MyMoneyFile::instance()->asset(), name); if (id.isEmpty()) id = locateSubAccount(MyMoneyFile::instance()->liability(), name); return id; } QString MyMoneyFile::parentName(const QString& name) const { return name.section(AccountSeparator, 0, -2); } QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const { MyMoneyAccount nextBase; QString level, remainder; level = category.section(AccountSeparator, 0, 0); remainder = category.section(AccountSeparator, 1); foreach (const auto sAccount, base.accountList()) { nextBase = account(sAccount); if (nextBase.name() == level) { if (remainder.isEmpty()) { return nextBase.id(); } return locateSubAccount(nextBase, remainder); } } return QString(); } QString MyMoneyFile::value(const QString& key) const { d->checkStorage(); return d->m_storage->value(key); } void MyMoneyFile::setValue(const QString& key, const QString& val) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setValue(key, val); } void MyMoneyFile::deletePair(const QString& key) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->deletePair(key); } void MyMoneyFile::addSchedule(MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); const auto splits = sched.transaction().splits(); for (const auto& split : splits) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts const auto acc = account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account"); } d->m_storage->addSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched); } void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); foreach (const auto split, sched.transaction().splits()) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account"); } d->m_storage->modifySchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched); } void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched); } MyMoneySchedule MyMoneyFile::schedule(const QString& id) const { return d->m_storage->schedule(id); } QList MyMoneyFile::scheduleList( const QString& accountId, const Schedule::Type type, const Schedule::Occurrence occurrence, const Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { d->checkStorage(); return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue); } QList MyMoneyFile::scheduleList( const QString& accountId) const { return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QList MyMoneyFile::scheduleList() const { return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QStringList MyMoneyFile::consistencyCheck() { QList list; QList::Iterator it_a; QList::Iterator it_sch; QList::Iterator it_p; QList::Iterator it_t; QList::Iterator it_r; QStringList accountRebuild; QMap interestAccounts; MyMoneyAccount parent; MyMoneyAccount child; MyMoneyAccount toplevel; QString parentId; QStringList rc; int problemCount = 0; int unfixedCount = 0; QString problemAccount; // check that we have a storage object d->checkTransaction(Q_FUNC_INFO); // get the current list of accounts accountList(list); // add the standard accounts list << MyMoneyFile::instance()->asset(); list << MyMoneyFile::instance()->liability(); list << MyMoneyFile::instance()->income(); list << MyMoneyFile::instance()->expense(); for (it_a = list.begin(); it_a != list.end(); ++it_a) { // no more checks for standard accounts if (isStandardAccount((*it_a).id())) { continue; } switch ((*it_a).accountGroup()) { case Account::Type::Asset: toplevel = asset(); break; case Account::Type::Liability: toplevel = liability(); break; case Account::Type::Expense: toplevel = expense(); break; case Account::Type::Income: toplevel = income(); break; case Account::Type::Equity: toplevel = equity(); break; default: qWarning("%s:%d This should never happen!", __FILE__ , __LINE__); break; } // check for loops in the hierarchy parentId = (*it_a).parentAccountId(); try { bool dropOut = false; while (!isStandardAccount(parentId) && !dropOut) { parent = account(parentId); if (parent.id() == (*it_a).id()) { // parent loops, so we need to re-parent to toplevel account // find parent account in our list problemCount++; QList::Iterator it_b; for (it_b = list.begin(); it_b != list.end(); ++it_b) { if ((*it_b).id() == parent.id()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name()); rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name()); (*it_a).setParentAccountId(toplevel.id()); if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); dropOut = true; break; } } } } parentId = parent.parentAccountId(); } } catch (const MyMoneyException &) { // if we don't know about a parent, we catch it later } // check that the parent exists parentId = (*it_a).parentAccountId(); try { parent = account(parentId); if ((*it_a).accountGroup() != parent.accountGroup()) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // the parent belongs to a different group, so we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name()); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account // and the one we removed this account from if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } else if (!parent.accountList().contains((*it_a).id())) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // parent exists, but does not have a reference to the account rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } } catch (const MyMoneyException &) { // apparently, the parent does not exist anymore. we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The parent with id %1 does not exist anymore.", parentId); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); } // now check that all the children exist and have the correct type foreach (const auto accountID, (*it_a).accountList()) { // check that the child exists try { child = account(accountID); if (child.parentAccountId() != (*it_a).id()) { throw MYMONEYEXCEPTION_CSTRING("Child account has a different parent"); } } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Child account with id %1 does not exist anymore.", accountID); rc << i18n(" The child account list will be reconstructed."); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); } } // see if it is a loan account. if so, remember the assigned interest account if ((*it_a).isLoan()) { MyMoneyAccountLoan loan(*it_a); if (!loan.interestAccountId().isEmpty()) { interestAccounts[loan.interestAccountId()] = true; } try { payee(loan.payee()); } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee()); rc << i18n(" The payee will be removed."); // remove the payee - the account will be modified in the engine later (*it_a).deletePair("payee"); } } // check if it is a category and set the date to 1900-01-01 if different if ((*it_a).isIncomeExpense()) { if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) { (*it_a).setOpeningDate(QDate(1900, 1, 1)); } } // check for clear text online password in the online settings if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext."); rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK."); rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version."); ++unfixedCount; } // if the account was modified, we need to update it in the engine if (!(d->m_storage->account((*it_a).id()) == (*it_a))) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data in engine."); return rc; } } } if (accountRebuild.count() != 0) { rc << i18n("* Reconstructing the child lists for"); } // clear the affected lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { rc << QString(" %1").arg((*it_a).name()); // clear the account list (*it_a).removeAccountIds(); } } // reconstruct the lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { QList::Iterator it; parentId = (*it_a).parentAccountId(); if (accountRebuild.contains(parentId)) { for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == parentId) { (*it).addAccountId((*it_a).id()); break; } } } } // update the engine objects for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name()); } } } // For some reason, files exist with invalid ids. This has been found in the payee id // so we fix them here QList pList = payeeList(); QMappayeeConversionMap; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if ((*it_p).id().length() > 7) { // found one of those with an invalid ids // create a new one and store it in the map. MyMoneyPayee payee = (*it_p); payee.clearId(); d->m_storage->addPayee(payee); payeeConversionMap[(*it_p).id()] = payee.id(); rc << i18n(" * Payee %1 recreated with fixed id", payee.name()); ++problemCount; } } // Fix the transactions MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); const auto tList = d->m_storage->transactionList(filter); // Generate the list of interest accounts for (const auto& transaction : tList) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) interestAccounts[split.accountId()] = true; } } QSet supportedAccountTypes; supportedAccountTypes << Account::Type::Checkings << Account::Type::Savings << Account::Type::Cash << Account::Type::CreditCard << Account::Type::Asset << Account::Type::Liability; QSet reportedUnsupportedAccounts; for (const auto& transaction : tList) { MyMoneyTransaction t = transaction; bool tChanged = false; QDate accountOpeningDate; QStringList accountList; const auto splits = t.splits(); foreach (const auto split, splits) { bool sChanged = false; MyMoneySplit s = split; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id()); ++problemCount; } try { const auto acc = this->account(s.accountId()); // compute the newest opening date of all accounts involved in the transaction // in case the newest opening date is newer than the transaction post date, do one // of the following: // // a) for category and stock accounts: update the opening date of the account // b) for account types where the user cannot modify the opening date through // the UI issue a warning (for each account only once) // c) others will be caught later if (!acc.isIncomeExpense() && !acc.isInvest()) { if (acc.openingDate() > t.postDate()) { if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) { accountOpeningDate = acc.openingDate(); } accountList << this->accountToCategory(acc.id()); if (!supportedAccountTypes.contains(acc.accountType()) && !reportedUnsupportedAccounts.contains(acc.id())) { rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.", this->accountToCategory(acc.id()), t.id()); reportedUnsupportedAccounts << acc.id(); ++unfixedCount; } } } else { if (acc.openingDate() > t.postDate()) { rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.", t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate)); rc << i18n(" Account opening date updated."); MyMoneyAccount newAcc = acc; newAcc.setOpeningDate(t.postDate()); this->modifyAccount(newAcc); ++problemCount; } } // make sure, that shares and value have the same number if they // represent the same currency. if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split of transaction '%1'.", t.id()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split of transaction '%1'.", t.id()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId()); ++unfixedCount; } // make sure the interest splits are marked correct as such if (interestAccounts.find(s.accountId()) != interestAccounts.end() && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); sChanged = true; rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id()); ++problemCount; } if (sChanged) { tChanged = true; t.modifySplit(s); } } // make sure that the transaction's post date is valid if (!t.postDate().isValid()) { tChanged = true; t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate()); rc << i18n(" * Transaction '%1' has an invalid post date.", t.id()); rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat)); ++problemCount; } // check if the transaction's post date is after the opening date // of all accounts involved in the transaction. In case it is not, // issue a warning with the details about the transaction incl. // the account names and dates involved if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) { QDate originalPostDate = t.postDate(); #if 0 // for now we do not activate the logic to move the post date to a later // point in time. This could cause some severe trouble if you have lots // of ancient data collected with older versions of KMyMoney that did not // enforce certain conditions like we do now. t.setPostDate(accountOpeningDate); tChanged = true; // copy the price information for investments to the new date QList::const_iterator it_t; foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = this->account(id); MyMoneySecurity sec = this->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); this->addPrice(price); break; } #endif rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat)); rc << i18n(" Referenced accounts: %1", accountList.join(",")); rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat)); ++unfixedCount; } if (tChanged) { d->m_storage->modifyTransaction(t); } } // Fix the schedules QList schList = scheduleList(); for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) { MyMoneySchedule sch = (*it_sch); MyMoneyTransaction t = sch.transaction(); auto tChanged = false; foreach (const auto split, t.splits()) { MyMoneySplit s = split; bool sChanged = false; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name()); ++problemCount; } if (!split.value().isZero() && split.shares().isZero()) { s.setShares(s.value()); sChanged = true; rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name()); rc << i18n(" Shares set to value."); ++problemCount; } // make sure, we don't have a bankid stored with a split in a schedule if (!split.bankID().isEmpty()) { s.setBankID(QString()); sChanged = true; rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name()); ++problemCount; } // make sure, that shares and value have the same number if they // represent the same currency. try { const auto acc = this->account(s.accountId()); if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId()); ++unfixedCount; } if (sChanged) { t.modifySplit(s); tChanged = true; } } if (tChanged) { sch.setTransaction(t); d->m_storage->modifySchedule(sch); } } // Fix the reports QList rList = reportList(); for (it_r = rList.begin(); it_r != rList.end(); ++it_r) { MyMoneyReport r = *it_r; QStringList payeeList; (*it_r).payees(payeeList); bool rChanged = false; for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) { if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; r.removeReference(*it_payee); r.addPayee(payeeConversionMap[*it_payee]); rChanged = true; } } if (rChanged) { d->m_storage->modifyReport(r); } } // erase old payee ids QMap::Iterator it_m; for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) { MyMoneyPayee payee = this->payee(it_m.key()); removePayee(payee); rc << i18n(" * Payee '%1' removed.", payee.id()); ++problemCount; } //look for accounts which have currencies other than the base currency but no price on the opening date //all accounts using base currency are excluded, since that's the base used for foreign currency calculation //thus it is considered as always present //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their //fake opening date since a forex rate is required for all multi-currency transactions //get all currencies in use QStringList currencyList; QList accountForeignCurrency; QList accList; accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { MyMoneyAccount account = *account_it; if (!account.isIncomeExpense() && !currencyList.contains(account.currencyId()) && account.currencyId() != baseCurrency().id() && !account.currencyId().isEmpty()) { //add the currency and the account-currency pair currencyList.append(account.currencyId()); accountForeignCurrency.append(account); } } MyMoneyPriceList pricesList = priceList(); QMap securityPriceDate; //get the first date of the price for each security MyMoneyPriceList::const_iterator prices_it; for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //only check the price if the currency is in use if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) { //check the security in the from field //if it is there, check if it is older QPair pricePair = qMakePair(firstPrice.from(), firstPrice.to()); securityPriceDate[pricePair] = firstPrice.date(); } } //compare the dates with the opening dates of the accounts using each currency QList::const_iterator accForeignList_it; bool firstInvProblem = true; for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) { //setup the price pair correctly QPair pricePair; //setup the reverse, which can also be used for rate conversion QPair reversePricePair; if ((*accForeignList_it).isInvest()) { //if it is a stock, we have to search for a price from its stock to the currency of the account QString securityId = (*accForeignList_it).currencyId(); QString tradingCurrencyId = security(securityId).tradingCurrency(); pricePair = qMakePair(securityId, tradingCurrencyId); reversePricePair = qMakePair(tradingCurrencyId, securityId); } else { //if it is a regular account we search for a price from the currency of the account to the base currency QString currency = (*accForeignList_it).currencyId(); QString baseCurrencyId = baseCurrency().id(); pricePair = qMakePair(currency, baseCurrencyId); reversePricePair = qMakePair(baseCurrencyId, currency); } //compare the first price with the opening date of the account if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate()) && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) { if (firstInvProblem) { firstInvProblem = false; - rc << i18n("* Potential problem with investments/currencies"); + rc << i18n("* Potential problem with securities/currencies"); } QDate openingDate = (*accForeignList_it).openingDate(); MyMoneySecurity secError = security((*accForeignList_it).currencyId()); if (!(*accForeignList_it).isInvest()) { rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the currency on or before the opening date."); } else { - rc << i18n(" * The investment '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); - rc << i18n(" Please enter a price for the investment on or before the opening date."); + rc << i18n(" * The security '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); + rc << i18n(" Please enter a price for the security on or before the opening date."); } ++unfixedCount; } } // Fix the budgets that somehow still reference invalid accounts QString problemBudget; QList bList = budgetList(); for (QList::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) { MyMoneyBudget b = *it_b; QList baccounts = b.getaccounts(); bool bChanged = false; for (QList::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) { try { account((*it_bacc).id()); } catch (const MyMoneyException &) { problemCount++; if (problemBudget != b.name()) { problemBudget = b.name(); rc << i18n("* Problem with budget '%1'", problemBudget); } rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id()); rc << i18n(" The account reference will be removed."); // remove the reference to the account b.removeReference((*it_bacc).id()); bChanged = true; } } if (bChanged) { d->m_storage->modifyBudget(b); } } // add more checks here if (problemCount == 0 && unfixedCount == 0) { rc << i18n("Finished: data is consistent."); } else { const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount); const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount); rc << QString(); rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining); } return rc; } QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent = base; QString categoryText; if (base.id() != expense().id() && base.id() != income().id()) throw MYMONEYEXCEPTION_CSTRING("Invalid base category"); QStringList subAccounts = name.split(AccountSeparator); QStringList::Iterator it; for (it = subAccounts.begin(); it != subAccounts.end(); ++it) { MyMoneyAccount categoryAccount; categoryAccount.setName(*it); categoryAccount.setAccountType(base.accountType()); if (it == subAccounts.begin()) categoryText += *it; else categoryText += (AccountSeparator + *it); // Only create the account if it doesn't exist try { QString categoryId = categoryToAccount(categoryText); if (categoryId.isEmpty()) addAccount(categoryAccount, parent); else { categoryAccount = account(categoryId); } } catch (const MyMoneyException &e) { qDebug("Unable to add account %s, %s, %s: %s", qPrintable(categoryAccount.name()), qPrintable(parent.name()), qPrintable(categoryText), e.what()); } parent = categoryAccount; } return categoryToAccount(name); } QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2) { QString accountId; MyMoneyAccount newAccount; bool found = true; if (!name.isEmpty()) { // The category might be constructed with an arbitrary depth (number of // colon delimited fields). We try to find a parent account within this // hierarchy by searching the following sequence: // // aaaa:bbbb:cccc:ddddd // // 1. search aaaa:bbbb:cccc:dddd, create nothing // 2. search aaaa:bbbb:cccc , create dddd // 3. search aaaa:bbbb , create cccc:dddd // 4. search aaaa , create bbbb:cccc:dddd // 5. don't search , create aaaa:bbbb:cccc:dddd newAccount.setName(name); QString accName; // part to be created (right side in above list) QString parent(name); // a possible parent part (left side in above list) do { accountId = categoryToAccount(parent); if (accountId.isEmpty()) { found = false; // prepare next step if (!accName.isEmpty()) accName.prepend(':'); accName.prepend(parent.section(':', -1)); newAccount.setName(accName); parent = parent.section(':', 0, -2); } else if (!accName.isEmpty()) { newAccount.setParentAccountId(accountId); } } while (!parent.isEmpty() && accountId.isEmpty()); // if we did not find the category, we create it if (!found) { MyMoneyAccount parentAccount; if (newAccount.parentAccountId().isEmpty()) { if (!value.isNegative() && value2.isNegative()) parentAccount = income(); else parentAccount = expense(); } else { parentAccount = account(newAccount.parentAccountId()); } newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense); MyMoneyAccount brokerage; // clear out the parent id, because createAccount() does not like that newAccount.setParentAccountId(QString()); createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney()); accountId = newAccount.id(); } } return accountId; } void MyMoneyFile::addSecurity(MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Add, security); } void MyMoneyFile::modifySecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifySecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security); } void MyMoneyFile::removeSecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // FIXME check that security is not referenced by other object d->m_storage->removeSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security); } MyMoneySecurity MyMoneyFile::security(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return baseCurrency(); return d->m_storage->security(id); } QList MyMoneyFile::securityList() const { d->checkStorage(); return d->m_storage->securityList(); } void MyMoneyFile::addCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency); } void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // force reload of base currency object if (currency.id() == d->m_baseCurrency.id()) d->m_baseCurrency.clearId(); d->m_storage->modifyCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency); } void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); if (currency.id() == d->m_baseCurrency.id()) throw MYMONEYEXCEPTION_CSTRING("Cannot delete base currency."); // FIXME check that security is not referenced by other object d->m_storage->removeCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency); } MyMoneySecurity MyMoneyFile::currency(const QString& id) const { if (id.isEmpty()) return baseCurrency(); try { const auto currency = d->m_storage->currency(id); if (currency.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id)); return currency; } catch (const MyMoneyException &) { const auto security = d->m_storage->security(id); if (security.id().isEmpty()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id)); } return security; } } QMap MyMoneyFile::ancientCurrencies() const { QMap ancientCurrencies; ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney"))); // http://en.wikipedia.org/wiki/Bulgarian_lev ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney"))); // Source: http://www.tf-portfoliosolutions.net/products/turkishlira.aspx ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney"))); // Source: http://en.wikipedia.org/wiki/Mozambican_metical ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); // Source https://en.wikipedia.org/wiki/Azerbaijani_manat ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Litas ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Belarusian_ruble ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); return ancientCurrencies; } QList MyMoneyFile::availableCurrencyList() const { QList currencyList; currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani"))); currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek"))); currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder"))); currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar"))); currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc"))); currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta"))); currencyList.append(MyMoneySecurity("AOA", i18n("Angolan Kwanza"), "Kz")); currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$")); currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin"))); currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$")); currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m.")); currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$")); currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000)); currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka"))); currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$")); currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC")); currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br")); currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$")); currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$")); currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum"))); currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano"))); currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark"))); currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula"))); currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$")); currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$")); currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)"))); currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc"))); currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC"))); currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO"))); currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100)); currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel"))); currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$")); currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo"))); currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso"))); currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi"))); currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso"))); currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc"))); currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1))); currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna"))); currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso"))); currencyList.append(MyMoneySecurity("CUC", i18n("Cuban Convertible Peso"))); currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna"))); currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr")); currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc"))); currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso"))); currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$")); currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon"))); currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa"))); currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon"))); currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr"))); currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac))); currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$")); currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi"))); currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari"))); currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi"))); currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal"))); currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso"))); currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$")); currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde"))); currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira"))); currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$")); currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100)); currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona"))); currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20A8))); currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1)); currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1)); currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000)); currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA))); currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$")); currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1)); currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000)); currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge"))); currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling"))); currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000)); currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som"))); currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD))); currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats"))); currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti"))); currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$")); currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000)); currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca"))); currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar"))); currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500)); currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha"))); currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit"))); currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa"))); currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc"))); currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5)); currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee"))); currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$")); currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu"))); currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE))); currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham"))); currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT")); currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat"))); currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$")); currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee"))); currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$")); currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro"))); currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6))); currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr")); currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000)); currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee"))); currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa"))); currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina"))); currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani"))); currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol"))); currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1))); currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty"))); currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial"))); currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)"))); currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble"))); currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc"))); currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala"))); currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra"))); currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal"))); currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar"))); currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee"))); currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone"))); currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$")); currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling"))); currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand"))); currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee"))); currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar"))); currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder"))); currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni"))); currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona"))); currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr")); currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$")); currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni"))); currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling"))); currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F))); currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga"))); currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$")); currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000)); currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA))); currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat"))); currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$")); currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling"))); currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia"))); currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo"))); currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham"))); currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso"))); currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum"))); currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu"))); currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar"))); currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB))); currencyList.append(MyMoneySecurity("ZMK", i18n("Zambian Kwacha"))); currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$")); currencyList.append(ancientCurrencies().keys()); // sort the currencies ... qSort(currencyList.begin(), currencyList.end(), [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2) { return c1.name().compare(c2.name()) < 0; }); // ... and add a few precious metals at the ned currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000)); currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000)); currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000)); currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000)); return currencyList; } QList MyMoneyFile::currencyList() const { d->checkStorage(); return d->m_storage->currencyList(); } QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const { if (baseCurrency().id() == second) return first; return second; } MyMoneySecurity MyMoneyFile::baseCurrency() const { if (d->m_baseCurrency.id().isEmpty()) { QString id = QString(value("kmm-baseCurrency")); if (!id.isEmpty()) d->m_baseCurrency = currency(id); } return d->m_baseCurrency; } void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr) { // make sure the currency exists MyMoneySecurity c = currency(curr.id()); if (c.id() != d->m_baseCurrency.id()) { setValue("kmm-baseCurrency", curr.id()); // force reload of base currency cache d->m_baseCurrency = MyMoneySecurity(); } } void MyMoneyFile::addPrice(const MyMoneyPrice& price) { if (price.rate(QString()).isZero()) return; d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->addPrice(price); } void MyMoneyFile::removePrice(const MyMoneyPrice& price) { d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->removePrice(price); } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const { d->checkStorage(); QString to(toId); if (to.isEmpty()) to = value("kmm-baseCurrency"); // if some id is missing, we can return an empty price object if (fromId.isEmpty() || to.isEmpty()) return MyMoneyPrice(); // we don't search our tables if someone asks stupid stuff if (fromId == toId) { return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney"); } // if not asking for exact date, try to find the exact date match first, // either the requested price or its reciprocal value. If unsuccessful, it will move // on and look for prices of previous dates MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true); if (!rc.isValid()) { // not found, search 'to-from' rate and use reciprocal value rc = d->m_storage->price(to, fromId, date, true); // not found, search previous dates, if exact date is not needed if (!exactDate && !rc.isValid()) { // search 'from-to' and 'to-from', select the most recent one MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate); MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate); // check first whether both prices are valid if (fromPrice.isValid() && toPrice.isValid()) { if (fromPrice.date() >= toPrice.date()) { // if 'from-to' is newer or the same date, prefer that one rc = fromPrice; } else { // otherwise, use the reciprocal price rc = toPrice; } } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one rc = fromPrice; } else if (toPrice.isValid()) { rc = toPrice; } } } return rc; } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const { return price(fromId, toId, QDate::currentDate(), false); } MyMoneyPrice MyMoneyFile::price(const QString& fromId) const { return price(fromId, QString(), QDate::currentDate(), false); } MyMoneyPriceList MyMoneyFile::priceList() const { d->checkStorage(); return d->m_storage->priceList(); } bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const { const auto accounts = account(id).accountList(); for (const auto& acc : accounts) { if (account(acc).name().compare(name) == 0) return true; } return false; } QList MyMoneyFile::reportList() const { d->checkStorage(); return d->m_storage->reportList(); } void MyMoneyFile::addReport(MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addReport(report); } void MyMoneyFile::modifyReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyReport(report); } unsigned MyMoneyFile::countReports() const { d->checkStorage(); return d->m_storage->countReports(); } MyMoneyReport MyMoneyFile::report(const QString& id) const { d->checkStorage(); return d->m_storage->report(id); } void MyMoneyFile::removeReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeReport(report); } QList MyMoneyFile::budgetList() const { d->checkStorage(); return d->m_storage->budgetList(); } void MyMoneyFile::addBudget(MyMoneyBudget &budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addBudget(budget); } MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const { d->checkStorage(); return d->m_storage->budgetByName(name); } void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyBudget(budget); } unsigned MyMoneyFile::countBudgets() const { d->checkStorage(); return d->m_storage->countBudgets(); } MyMoneyBudget MyMoneyFile::budget(const QString& id) const { d->checkStorage(); return d->m_storage->budget(id); } void MyMoneyFile::removeBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeBudget(budget); } void MyMoneyFile::addOnlineJob(onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Add, job); } void MyMoneyFile::modifyOnlineJob(const onlineJob job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job); } onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const { d->checkStorage(); return d->m_storage->getOnlineJob(jobId); } QList MyMoneyFile::onlineJobList() const { d->checkStorage(); return d->m_storage->onlineJobList(); } /** @todo improve speed by passing count job to m_storage */ int MyMoneyFile::countOnlineJobs() const { return onlineJobList().count(); } /** * @brief Remove onlineJob * @param job onlineJob to remove */ void MyMoneyFile::removeOnlineJob(const onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache if (job.isLocked()) { return; } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job); d->m_storage->removeOnlineJob(job); } void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds) { foreach (QString jobId, onlineJobIds) { removeOnlineJob(getOnlineJob(jobId)); } } void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const { d->checkStorage(); list = d->m_storage->costCenterList(); } void MyMoneyFile::updateVAT(MyMoneyTransaction& transaction) const { // check if transaction qualifies const auto splitCount = transaction.splits().count(); if (splitCount > 1 && splitCount <= 3) { MyMoneyMoney amount; MyMoneyAccount assetLiability; MyMoneyAccount category; MyMoneySplit taxSplit; const QString currencyId = transaction.commodity(); foreach (const auto& split, transaction.splits()) { const auto acc = account(split.accountId()); // all splits must reference accounts denoted in the same currency if (acc.currencyId() != currencyId) { return; } if (acc.isAssetLiability() && assetLiability.id().isEmpty()) { amount = split.shares(); assetLiability = acc; continue; } if (acc.isAssetLiability()) { return; } if (category.id().isEmpty() && !acc.value("VatAccount").isEmpty()) { category = acc; continue; } else if(taxSplit.id().isEmpty() && !acc.value("Tax").toLower().compare(QLatin1String("yes"))) { taxSplit = split; continue; } return; } if (!category.id().isEmpty()) { // remove a possibly found tax split - we create a new one // but only if it is the same tax category if (!taxSplit.id().isEmpty()) { if (category.value("VatAccount").compare(taxSplit.accountId())) return; transaction.removeSplit(taxSplit); } addVATSplit(transaction, assetLiability, category, amount); } } } bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& acc, const MyMoneyAccount& category, const MyMoneyMoney& amount) const { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; MyMoneyAccount vatAcc = account(category.value("VatAccount")); const MyMoneySecurity& asec = security(acc.currencyId()); const MyMoneySecurity& csec = security(category.currencyId()); const MyMoneySecurity& vsec = security(vatAcc.currencyId()); if (asec.id() != csec.id() || asec.id() != vsec.id()) { qDebug("Auto VAT assignment only works if all three accounts use the same currency."); return false; } MyMoneyMoney vatRate(vatAcc.value("VatRate")); MyMoneyMoney gv, nv; // gross value, net value int fract = acc.fraction(); if (!vatRate.isZero()) { tax.setAccountId(vatAcc.id()); // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1()); if (category.value("VatAmount").toLower() != QString("net")) { // split value is the gross value gv = amount; nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit catSplit = transaction.splitByAccount(acc.id(), false); catSplit.setShares(-nv); catSplit.setValue(catSplit.shares()); transaction.modifySplit(catSplit); } else { // split value is the net value nv = amount; gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit accSplit = transaction.splitByAccount(acc.id()); accSplit.setValue(gv.convert(fract)); accSplit.setShares(accSplit.value()); transaction.modifySplit(accSplit); } tax.setValue(-(gv - nv).convert(fract)); tax.setShares(tax.value()); transaction.addSplit(tax); rc = true; } } catch (const MyMoneyException &) { } return rc; } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const { d->checkStorage(); return d->m_storage->isReferenced(obj, skipChecks); } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const { return isReferenced(obj, QBitArray((int)eStorage::Reference::Count)); } bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const { // by definition, an empty string or a non-numeric string is not used QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?")); if (no.isEmpty() || exp.indexIn(no) == -1) return false; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { MyMoneySplit split; // Test whether the transaction also includes a split into // this account split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty() && split.number() == no) return true; } catch (const MyMoneyException &) { } ++it_t; } return false; } QString MyMoneyFile::highestCheckNo(const QString& accId) const { unsigned64 lno = 0; unsigned64 cno; QString no; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty()) { // non-numerical values stored in number will return 0 in the next line cno = split.number().toULongLong(); if (cno > lno) { lno = cno; no = split.number(); } } } catch (const MyMoneyException &) { } ++it_t; } return no; } bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.setDateFilter(date.addDays(+1), QDate()); return !transactionList(filter).isEmpty(); } void MyMoneyFile::clearCache() { d->checkStorage(); d->m_balanceCache.clear(); } void MyMoneyFile::forceDataChanged() { emit dataChanged(); } bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const { auto rc = true; if (t.splitCount() == 2) { foreach (const auto split, t.splits()) { auto acc = account(split.accountId()); if (acc.isIncomeExpense()) { rc = false; break; } } } return rc; } bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const { auto ret = false; foreach (const auto split, t.splits()) { if (referencesClosedAccount(split)) { ret = true; break; } } return ret; } bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const { if (s.accountId().isEmpty()) return false; try { return account(s.accountId()).isClosed(); } catch (const MyMoneyException &) { } return false; } QString MyMoneyFile::storageId() { QString id = value("kmm-id"); if (id.isEmpty()) { MyMoneyFileTransaction ft; try { QUuid uid = QUuid::createUuid(); setValue("kmm-id", uid.toString()); ft.commit(); id = uid.toString(); } catch (const MyMoneyException &) { qDebug("Unable to setup UID for new storage object"); } } return id; } QString MyMoneyFile::openingBalancesPrefix() { return i18n("Opening Balances"); } bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const { // get current values auto acc = account(_acc.id()); // if there's no last transaction import data we are done if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) return false; // otherwise, we compare the balances MyMoneyMoney balance(acc.value("lastStatementBalance")); MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate)); return balance == accBalance; } int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.addState((int)state); return transactionList(filter).count(); } QMap > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const { QMap > result; MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); d->checkStorage(); QList list; accountList(list); for (const auto& account : list) { result[account.id()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto transactions = d->m_storage->transactionList(filter); for (const auto& transaction : transactions) { const auto& splits = transaction.splits(); for (const auto& split : splits) { if (!result.contains(split.accountId())) { result[split.accountId()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto flag = split.reconcileFlag(); switch(flag) { case eMyMoney::Split::State::NotReconciled: case eMyMoney::Split::State::Cleared: case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: result[split.accountId()][(int)flag]++; break; default: break; } } } return result; } /** * Make sure that the splits value has the precision of the corresponding account */ void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const { auto transactionSecurity = security(t.commodity()); auto transactionFraction = transactionSecurity.smallestAccountFraction(); for (auto& split : t.splits()) { auto acc = account(split.accountId()); auto fraction = acc.fraction(); if(fraction == -1) { auto sec = security(acc.currencyId()); fraction = acc.fraction(sec); } // Don't do any rounding on a split factor if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { split.setShares(static_cast(split.shares().convertDenominator(fraction).canonicalize())); split.setValue(static_cast(split.value().convertDenominator(transactionFraction).canonicalize())); } } } class MyMoneyFileTransactionPrivate { Q_DISABLE_COPY(MyMoneyFileTransactionPrivate) public: MyMoneyFileTransactionPrivate() : m_isNested(MyMoneyFile::instance()->hasTransaction()), m_needRollback(!m_isNested) { } public: bool m_isNested; bool m_needRollback; }; MyMoneyFileTransaction::MyMoneyFileTransaction() : d_ptr(new MyMoneyFileTransactionPrivate) { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } MyMoneyFileTransaction::~MyMoneyFileTransaction() { try { rollback(); } catch (const MyMoneyException &e) { qDebug() << e.what(); } Q_D(MyMoneyFileTransaction); delete d; } void MyMoneyFileTransaction::restart() { rollback(); Q_D(MyMoneyFileTransaction); d->m_needRollback = !d->m_isNested; if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } void MyMoneyFileTransaction::commit() { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->commitTransaction(); d->m_needRollback = false; } void MyMoneyFileTransaction::rollback() { Q_D(MyMoneyFileTransaction); if (d->m_needRollback) MyMoneyFile::instance()->rollbackTransaction(); d->m_needRollback = false; } diff --git a/kmymoney/mymoney/mymoneytransactionfilter.cpp b/kmymoney/mymoney/mymoneytransactionfilter.cpp index 46bb4a736..88cfd391f 100644 --- a/kmymoney/mymoney/mymoneytransactionfilter.cpp +++ b/kmymoney/mymoney/mymoneytransactionfilter.cpp @@ -1,1013 +1,1026 @@ /* * Copyright 2003-2019 Thomas Baumgart * Copyright 2004 Ace Jones * Copyright 2008-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneytransactionfilter.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyenums.h" class MyMoneyTransactionFilterPrivate { public: MyMoneyTransactionFilterPrivate() : m_reportAllSplits(false) , m_considerCategory(false) , m_matchOnly(false) + , m_treatTransfersAsIncomeExpense(false) , m_matchingSplitsCount(0) , m_invertText(false) { m_filterSet.allFilter = 0; } MyMoneyTransactionFilter::FilterSet m_filterSet; bool m_reportAllSplits; bool m_considerCategory; bool m_matchOnly; + bool m_treatTransfersAsIncomeExpense; uint m_matchingSplitsCount; QRegExp m_text; bool m_invertText; QHash m_accounts; QHash m_payees; QHash m_tags; QHash m_categories; QHash m_states; QHash m_types; QHash m_validity; QString m_fromNr, m_toNr; QDate m_fromDate, m_toDate; MyMoneyMoney m_fromAmount, m_toAmount; }; MyMoneyTransactionFilter::MyMoneyTransactionFilter() : d_ptr(new MyMoneyTransactionFilterPrivate) { Q_D(MyMoneyTransactionFilter); d->m_reportAllSplits = true; d->m_considerCategory = true; } MyMoneyTransactionFilter::MyMoneyTransactionFilter(const QString& id) : d_ptr(new MyMoneyTransactionFilterPrivate) { addAccount(id); } MyMoneyTransactionFilter::MyMoneyTransactionFilter(const MyMoneyTransactionFilter& other) : d_ptr(new MyMoneyTransactionFilterPrivate(*other.d_func())) { } MyMoneyTransactionFilter::~MyMoneyTransactionFilter() { Q_D(MyMoneyTransactionFilter); delete d; } void MyMoneyTransactionFilter::clear() { Q_D(MyMoneyTransactionFilter); d->m_filterSet.allFilter = 0; d->m_invertText = false; d->m_accounts.clear(); d->m_categories.clear(); d->m_payees.clear(); d->m_tags.clear(); d->m_types.clear(); d->m_states.clear(); d->m_validity.clear(); d->m_fromDate = QDate(); d->m_toDate = QDate(); } void MyMoneyTransactionFilter::clearAccountFilter() { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.accountFilter = 0; d->m_accounts.clear(); } void MyMoneyTransactionFilter::setTextFilter(const QRegExp& text, bool invert) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.textFilter = 1; d->m_invertText = invert; d->m_text = text; } void MyMoneyTransactionFilter::addAccount(const QStringList& ids) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.accountFilter = 1; for (const auto& id : ids) addAccount(id); } void MyMoneyTransactionFilter::addAccount(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_accounts.isEmpty() && !id.isEmpty() && d->m_accounts.contains(id)) return; d->m_filterSet.singleFilter.accountFilter = 1; if (!id.isEmpty()) d->m_accounts.insert(id, QString()); } void MyMoneyTransactionFilter::addCategory(const QStringList& ids) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.categoryFilter = 1; for (const auto& id : ids) addCategory(id); } void MyMoneyTransactionFilter::addCategory(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_categories.isEmpty() && !id.isEmpty() && d->m_categories.contains(id)) return; d->m_filterSet.singleFilter.categoryFilter = 1; if (!id.isEmpty()) d->m_categories.insert(id, QString()); } void MyMoneyTransactionFilter::setDateFilter(const QDate& from, const QDate& to) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.dateFilter = from.isValid() | to.isValid(); d->m_fromDate = from; d->m_toDate = to; } void MyMoneyTransactionFilter::setAmountFilter(const MyMoneyMoney& from, const MyMoneyMoney& to) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.amountFilter = 1; d->m_fromAmount = from.abs(); d->m_toAmount = to.abs(); // make sure that the user does not try to fool us ;-) if (from > to) std::swap(d->m_fromAmount, d->m_toAmount); } void MyMoneyTransactionFilter::addPayee(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_payees.isEmpty() && !id.isEmpty() && d->m_payees.contains(id)) return; d->m_filterSet.singleFilter.payeeFilter = 1; if (!id.isEmpty()) d->m_payees.insert(id, QString()); } void MyMoneyTransactionFilter::addTag(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_tags.isEmpty() && !id.isEmpty() && d->m_tags.contains(id)) return; d->m_filterSet.singleFilter.tagFilter = 1; if (!id.isEmpty()) d->m_tags.insert(id, QString()); } void MyMoneyTransactionFilter::addType(const int type) { Q_D(MyMoneyTransactionFilter); if (!d->m_types.isEmpty() && d->m_types.contains(type)) return; d->m_filterSet.singleFilter.typeFilter = 1; d->m_types.insert(type, QString()); } void MyMoneyTransactionFilter::addState(const int state) { Q_D(MyMoneyTransactionFilter); if (!d->m_states.isEmpty() && d->m_states.contains(state)) return; d->m_filterSet.singleFilter.stateFilter = 1; d->m_states.insert(state, QString()); } void MyMoneyTransactionFilter::addValidity(const int type) { Q_D(MyMoneyTransactionFilter); if (!d->m_validity.isEmpty() && d->m_validity.contains(type)) return; d->m_filterSet.singleFilter.validityFilter = 1; d->m_validity.insert(type, QString()); } void MyMoneyTransactionFilter::setNumberFilter(const QString& from, const QString& to) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.nrFilter = 1; d->m_fromNr = from; d->m_toNr = to; } void MyMoneyTransactionFilter::setReportAllSplits(const bool report) { Q_D(MyMoneyTransactionFilter); d->m_reportAllSplits = report; } void MyMoneyTransactionFilter::setConsiderCategory(const bool check) { Q_D(MyMoneyTransactionFilter); d->m_considerCategory = check; } +void MyMoneyTransactionFilter::setTreatTransfersAsIncomeExpense(const bool check) +{ + Q_D(MyMoneyTransactionFilter); + d->m_treatTransfersAsIncomeExpense = check; +} + +bool MyMoneyTransactionFilter::treatTransfersAsIncomeExpense() const +{ + Q_D(const MyMoneyTransactionFilter); + return d->m_treatTransfersAsIncomeExpense; +} + uint MyMoneyTransactionFilter::matchingSplitsCount(const MyMoneyTransaction& transaction) { Q_D(MyMoneyTransactionFilter); d->m_matchOnly = true; matchingSplits(transaction); d->m_matchOnly = false; return d->m_matchingSplitsCount; } QVector MyMoneyTransactionFilter::matchingSplits(const MyMoneyTransaction& transaction) { Q_D(MyMoneyTransactionFilter); QVector matchingSplits; const auto file = MyMoneyFile::instance(); // qDebug("T: %s", transaction.id().data()); // if no filter is set, we can safely return a match // if we should report all splits, then we collect them if (!d->m_filterSet.allFilter && d->m_reportAllSplits) { d->m_matchingSplitsCount = transaction.splitCount(); if (!d->m_matchOnly) matchingSplits = QVector::fromList(transaction.splits()); return matchingSplits; } d->m_matchingSplitsCount = 0; const auto filter = d->m_filterSet.singleFilter; // perform checks on the MyMoneyTransaction object first // check the date range if (filter.dateFilter) { if ((d->m_fromDate != QDate() && transaction.postDate() < d->m_fromDate) || (d->m_toDate != QDate() && transaction.postDate() > d->m_toDate)) { return matchingSplits; } } auto categoryMatched = !filter.categoryFilter; auto accountMatched = !filter.accountFilter; auto isTransfer = true; // check the transaction's validity if (filter.validityFilter) { if (!d->m_validity.isEmpty() && !d->m_validity.contains((int)validTransaction(transaction))) return matchingSplits; } // if d->m_reportAllSplits == false.. // ...then we don't need splits... // ...but we need to know if there were any found auto isMatchingSplitsEmpty = true; auto extendedFilter = d->m_filterSet; extendedFilter.singleFilter.dateFilter = 0; extendedFilter.singleFilter.accountFilter = 0; extendedFilter.singleFilter.categoryFilter = 0; if (filter.accountFilter || filter.categoryFilter || extendedFilter.allFilter) { const auto& splits = transaction.splits(); for (const auto& s : splits) { if (filter.accountFilter || filter.categoryFilter) { auto removeSplit = true; if (d->m_considerCategory) { switch (file->account(s.accountId()).accountGroup()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: isTransfer = false; // check if the split references one of the categories in the list if (filter.categoryFilter) { if (d->m_categories.isEmpty()) { // we're looking for transactions with 'no' categories d->m_matchingSplitsCount = 0; matchingSplits.clear(); return matchingSplits; } else if (d->m_categories.contains(s.accountId())) { categoryMatched = true; removeSplit = false; } } break; default: // check if the split references one of the accounts in the list if (!filter.accountFilter) { removeSplit = false; } else if (!d->m_accounts.isEmpty() && d->m_accounts.contains(s.accountId())) { accountMatched = true; removeSplit = false; } break; } } else { if (!filter.accountFilter) { removeSplit = false; } else if (!d->m_accounts.isEmpty() && d->m_accounts.contains(s.accountId())) { accountMatched = true; removeSplit = false; } } if (removeSplit) continue; } // check if less frequent filters are active if (extendedFilter.allFilter) { const auto acc = file->account(s.accountId()); if (!(matchAmount(s) && matchText(s, acc))) continue; // Determine if this account is a category or an account auto isCategory = false; switch (acc.accountGroup()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: isCategory = true; default: break; } if (!isCategory) { // check the payee list if (filter.payeeFilter) { if (!d->m_payees.isEmpty()) { if (s.payeeId().isEmpty() || !d->m_payees.contains(s.payeeId())) continue; } else if (!s.payeeId().isEmpty()) continue; } // check the tag list if (filter.tagFilter) { const auto tags = s.tagIdList(); if (!d->m_tags.isEmpty()) { if (tags.isEmpty()) { continue; } else { auto found = false; for (const auto& tag : tags) { if (d->m_tags.contains(tag)) { found = true; break; } } if (!found) continue; } } else if (!tags.isEmpty()) continue; } // check the type list if (filter.typeFilter && !d->m_types.isEmpty() && !d->m_types.contains(splitType(transaction, s, acc))) continue; // check the state list if (filter.stateFilter && !d->m_states.isEmpty() && !d->m_states.contains(splitState(s))) continue; if (filter.nrFilter && ((!d->m_fromNr.isEmpty() && s.number() < d->m_fromNr) || (!d->m_toNr.isEmpty() && s.number() > d->m_toNr))) continue; } else if (filter.payeeFilter || filter.tagFilter || filter.typeFilter || filter.stateFilter || filter.nrFilter) { continue; } } if (d->m_reportAllSplits) matchingSplits.append(s); isMatchingSplitsEmpty = false; } } else if (d->m_reportAllSplits) { const auto& splits = transaction.splits(); for (const auto& s : splits) matchingSplits.append(s); d->m_matchingSplitsCount = matchingSplits.count(); return matchingSplits; } else if (transaction.splitCount() > 0) { isMatchingSplitsEmpty = false; } // check if we're looking for transactions without assigned category if (!categoryMatched && transaction.splitCount() == 1 && d->m_categories.isEmpty()) categoryMatched = true; // if there's no category filter and the category did not // match, then we still want to see this transaction if it's // a transfer if (!categoryMatched && !filter.categoryFilter) categoryMatched = isTransfer; if (isMatchingSplitsEmpty || !(accountMatched && categoryMatched)) { d->m_matchingSplitsCount = 0; return matchingSplits; } if (!d->m_reportAllSplits && !isMatchingSplitsEmpty) { d->m_matchingSplitsCount = 1; if (!d->m_matchOnly) matchingSplits.append(transaction.firstSplit()); } else { d->m_matchingSplitsCount = matchingSplits.count(); } // all filters passed, I guess we have a match // qDebug(" C: %d", m_matchingSplits.count()); return matchingSplits; } QDate MyMoneyTransactionFilter::fromDate() const { Q_D(const MyMoneyTransactionFilter); return d->m_fromDate; } QDate MyMoneyTransactionFilter::toDate() const { Q_D(const MyMoneyTransactionFilter); return d->m_toDate; } bool MyMoneyTransactionFilter::matchText(const MyMoneySplit& s, const MyMoneyAccount& acc) const { Q_D(const MyMoneyTransactionFilter); // check if the text is contained in one of the fields // memo, value, number, payee, tag, account if (d->m_filterSet.singleFilter.textFilter) { const auto file = MyMoneyFile::instance(); const auto sec = file->security(acc.currencyId()); if (s.memo().contains(d->m_text) || s.shares().formatMoney(acc.fraction(sec)).contains(d->m_text) || s.value().formatMoney(acc.fraction(sec)).contains(d->m_text) || s.number().contains(d->m_text) || (d->m_text.pattern().compare(s.transactionId())) == 0) return !d->m_invertText; if (acc.name().contains(d->m_text)) return !d->m_invertText; if (!s.payeeId().isEmpty() && file->payee(s.payeeId()).name().contains(d->m_text)) return !d->m_invertText; const auto& tagIdList = s.tagIdList(); for (const auto& tag : tagIdList) if (file->tag(tag).name().contains(d->m_text)) return !d->m_invertText; return d->m_invertText; } return true; } bool MyMoneyTransactionFilter::matchAmount(const MyMoneySplit& s) const { Q_D(const MyMoneyTransactionFilter); if (d->m_filterSet.singleFilter.amountFilter) { const auto value = s.value().abs(); const auto shares = s.shares().abs(); if ((value < d->m_fromAmount || value > d->m_toAmount) && (shares < d->m_fromAmount || shares > d->m_toAmount)) return false; } return true; } bool MyMoneyTransactionFilter::match(const MyMoneySplit& s) const { const auto& acc = MyMoneyFile::instance()->account(s.accountId()); return matchText(s, acc) && matchAmount(s); } bool MyMoneyTransactionFilter::match(const MyMoneyTransaction& transaction) { Q_D(MyMoneyTransactionFilter); d->m_matchOnly = true; matchingSplits(transaction); d->m_matchOnly = false; return d->m_matchingSplitsCount > 0; } int MyMoneyTransactionFilter::splitState(const MyMoneySplit& split) const { switch (split.reconcileFlag()) { default: case eMyMoney::Split::State::NotReconciled: return (int)eMyMoney::TransactionFilter::State::NotReconciled; case eMyMoney::Split::State::Cleared: return (int)eMyMoney::TransactionFilter::State::Cleared; case eMyMoney::Split::State::Reconciled: return (int)eMyMoney::TransactionFilter::State::Reconciled; case eMyMoney::Split::State::Frozen: return (int)eMyMoney::TransactionFilter::State::Frozen; } } int MyMoneyTransactionFilter::splitType(const MyMoneyTransaction& t, const MyMoneySplit& split, const MyMoneyAccount& acc) const { - qDebug() << "SplitType"; + Q_D(const MyMoneyTransactionFilter); if (acc.isIncomeExpense()) return (int)eMyMoney::TransactionFilter::Type::All; - if (t.splitCount() == 2) { + if (t.splitCount() == 2 && !d->m_treatTransfersAsIncomeExpense) { const auto& splits = t.splits(); const auto file = MyMoneyFile::instance(); const auto& a = splits.at(0).id().compare(split.id()) == 0 ? acc : file->account(splits.at(0).accountId()); const auto& b = splits.at(1).id().compare(split.id()) == 0 ? acc : file->account(splits.at(1).accountId()); - qDebug() << "first split: " << splits.at(0).accountId() << "second split: " << splits.at(1).accountId(); if (!a.isIncomeExpense() && !b.isIncomeExpense()) return (int)eMyMoney::TransactionFilter::Type::Transfers; } if (split.value().isPositive()) return (int)eMyMoney::TransactionFilter::Type::Deposits; return (int)eMyMoney::TransactionFilter::Type::Payments; } eMyMoney::TransactionFilter::Validity MyMoneyTransactionFilter::validTransaction(const MyMoneyTransaction& t) const { MyMoneyMoney val; const auto& splits = t.splits(); for (const auto& split : splits) val += split.value(); return (val == MyMoneyMoney()) ? eMyMoney::TransactionFilter::Validity::Valid : eMyMoney::TransactionFilter::Validity::Invalid; } bool MyMoneyTransactionFilter::includesCategory(const QString& cat) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.categoryFilter || d->m_categories.contains(cat); } bool MyMoneyTransactionFilter::includesAccount(const QString& acc) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.accountFilter || d->m_accounts.contains(acc); } bool MyMoneyTransactionFilter::includesPayee(const QString& pye) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.payeeFilter || d->m_payees.contains(pye); } bool MyMoneyTransactionFilter::includesTag(const QString& tag) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.tagFilter || d->m_tags.contains(tag); } bool MyMoneyTransactionFilter::dateFilter(QDate& from, QDate& to) const { Q_D(const MyMoneyTransactionFilter); from = d->m_fromDate; to = d->m_toDate; return d->m_filterSet.singleFilter.dateFilter == 1; } bool MyMoneyTransactionFilter::amountFilter(MyMoneyMoney& from, MyMoneyMoney& to) const { Q_D(const MyMoneyTransactionFilter); from = d->m_fromAmount; to = d->m_toAmount; return d->m_filterSet.singleFilter.amountFilter == 1; } bool MyMoneyTransactionFilter::numberFilter(QString& from, QString& to) const { Q_D(const MyMoneyTransactionFilter); from = d->m_fromNr; to = d->m_toNr; return d->m_filterSet.singleFilter.nrFilter == 1; } bool MyMoneyTransactionFilter::payees(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.payeeFilter; if (result) { QHashIterator it_payee(d->m_payees); while (it_payee.hasNext()) { it_payee.next(); list += it_payee.key(); } } return result; } bool MyMoneyTransactionFilter::tags(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.tagFilter; if (result) { QHashIterator it_tag(d->m_tags); while (it_tag.hasNext()) { it_tag.next(); list += it_tag.key(); } } return result; } bool MyMoneyTransactionFilter::accounts(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.accountFilter; if (result) { QHashIterator it_account(d->m_accounts); while (it_account.hasNext()) { it_account.next(); QString account = it_account.key(); list += account; } } return result; } bool MyMoneyTransactionFilter::categories(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.categoryFilter; if (result) { QHashIterator it_category(d->m_categories); while (it_category.hasNext()) { it_category.next(); list += it_category.key(); } } return result; } bool MyMoneyTransactionFilter::types(QList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.typeFilter; if (result) { QHashIterator it_type(d->m_types); while (it_type.hasNext()) { it_type.next(); list += it_type.key(); } } return result; } bool MyMoneyTransactionFilter::states(QList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.stateFilter; if (result) { QHashIterator it_state(d->m_states); while (it_state.hasNext()) { it_state.next(); list += it_state.key(); } } return result; } bool MyMoneyTransactionFilter::validities(QList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.validityFilter; if (result) { QHashIterator it_validity(d->m_validity); while (it_validity.hasNext()) { it_validity.next(); list += it_validity.key(); } } return result; } bool MyMoneyTransactionFilter::firstType(int&i) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.typeFilter; if (result) { QHashIterator it_type(d->m_types); if (it_type.hasNext()) { it_type.next(); i = it_type.key(); } } return result; } bool MyMoneyTransactionFilter::firstState(int&i) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.stateFilter; if (result) { QHashIterator it_state(d->m_states); if (it_state.hasNext()) { it_state.next(); i = it_state.key(); } } return result; } bool MyMoneyTransactionFilter::firstValidity(int&i) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.validityFilter; if (result) { QHashIterator it_validity(d->m_validity); if (it_validity.hasNext()) { it_validity.next(); i = it_validity.key(); } } return result; } bool MyMoneyTransactionFilter::textFilter(QRegExp& exp) const { Q_D(const MyMoneyTransactionFilter); exp = d->m_text; return d->m_filterSet.singleFilter.textFilter == 1; } bool MyMoneyTransactionFilter::isInvertingText() const { Q_D(const MyMoneyTransactionFilter); return d->m_invertText; } void MyMoneyTransactionFilter::setDateFilter(eMyMoney::TransactionFilter::Date range) { QDate from, to; if (translateDateRange(range, from, to)) setDateFilter(from, to); } static int fiscalYearStartMonth = 1; static int fiscalYearStartDay = 1; void MyMoneyTransactionFilter::setFiscalYearStart(int firstMonth, int firstDay) { fiscalYearStartMonth = firstMonth; fiscalYearStartDay = firstDay; } bool MyMoneyTransactionFilter::translateDateRange(eMyMoney::TransactionFilter::Date id, QDate& start, QDate& end) { bool rc = true; int yr = QDate::currentDate().year(); int mon = QDate::currentDate().month(); switch (id) { case eMyMoney::TransactionFilter::Date::All: start = QDate(); end = QDate(); break; case eMyMoney::TransactionFilter::Date::AsOfToday: start = QDate(); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::CurrentMonth: start = QDate(yr, mon, 1); end = QDate(yr, mon, 1).addMonths(1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::CurrentYear: start = QDate(yr, 1, 1); end = QDate(yr, 12, 31); break; case eMyMoney::TransactionFilter::Date::MonthToDate: start = QDate(yr, mon, 1); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::YearToDate: start = QDate(yr, 1, 1); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::YearToMonth: start = QDate(yr, 1, 1); end = QDate(yr, mon, 1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastMonth: start = QDate(yr, mon, 1).addMonths(-1); end = QDate(yr, mon, 1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastYear: start = QDate(yr, 1, 1).addYears(-1); end = QDate(yr, 12, 31).addYears(-1); break; case eMyMoney::TransactionFilter::Date::Last7Days: start = QDate::currentDate().addDays(-7); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last30Days: start = QDate::currentDate().addDays(-30); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last3Months: start = QDate::currentDate().addMonths(-3); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last6Months: start = QDate::currentDate().addMonths(-6); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last11Months: start = QDate(yr, mon, 1).addMonths(-12); end = QDate(yr, mon, 1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::Last12Months: start = QDate::currentDate().addMonths(-12); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Next7Days: start = QDate::currentDate(); end = QDate::currentDate().addDays(7); break; case eMyMoney::TransactionFilter::Date::Next30Days: start = QDate::currentDate(); end = QDate::currentDate().addDays(30); break; case eMyMoney::TransactionFilter::Date::Next3Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(3); break; case eMyMoney::TransactionFilter::Date::Next6Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(6); break; case eMyMoney::TransactionFilter::Date::Next12Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(12); break; case eMyMoney::TransactionFilter::Date::Next18Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(18); break; case eMyMoney::TransactionFilter::Date::UserDefined: start = QDate(); end = QDate(); break; case eMyMoney::TransactionFilter::Date::Last3ToNext3Months: start = QDate::currentDate().addMonths(-3); end = QDate::currentDate().addMonths(3); break; case eMyMoney::TransactionFilter::Date::CurrentQuarter: start = QDate(yr, mon - ((mon - 1) % 3), 1); end = start.addMonths(3).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastQuarter: start = QDate(yr, mon - ((mon - 1) % 3), 1).addMonths(-3); end = start.addMonths(3).addDays(-1); break; case eMyMoney::TransactionFilter::Date::NextQuarter: start = QDate(yr, mon - ((mon - 1) % 3), 1).addMonths(3); end = start.addMonths(3).addDays(-1); break; case eMyMoney::TransactionFilter::Date::CurrentFiscalYear: start = QDate(QDate::currentDate().year(), fiscalYearStartMonth, fiscalYearStartDay); if (QDate::currentDate() < start) start = start.addYears(-1); end = start.addYears(1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastFiscalYear: start = QDate(QDate::currentDate().year(), fiscalYearStartMonth, fiscalYearStartDay); if (QDate::currentDate() < start) start = start.addYears(-1); start = start.addYears(-1); end = start.addYears(1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::Today: start = QDate::currentDate(); end = QDate::currentDate(); break; default: qWarning("Unknown date identifier %d in MyMoneyTransactionFilter::translateDateRange()", (int)id); rc = false; break; } return rc; } MyMoneyTransactionFilter::FilterSet MyMoneyTransactionFilter::filterSet() const { Q_D(const MyMoneyTransactionFilter); return d->m_filterSet; } void MyMoneyTransactionFilter::removeReference(const QString& id) { Q_D(MyMoneyTransactionFilter); if (d->m_accounts.end() != d->m_accounts.find(id)) { qDebug("%s", qPrintable(QString("Remove account '%1' from report").arg(id))); d->m_accounts.take(id); } else if (d->m_categories.end() != d->m_categories.find(id)) { qDebug("%s", qPrintable(QString("Remove category '%1' from report").arg(id))); d->m_categories.remove(id); } else if (d->m_payees.end() != d->m_payees.find(id)) { qDebug("%s", qPrintable(QString("Remove payee '%1' from report").arg(id))); d->m_payees.remove(id); } else if (d->m_tags.end() != d->m_tags.find(id)) { qDebug("%s", qPrintable(QString("Remove tag '%1' from report").arg(id))); d->m_tags.remove(id); } } diff --git a/kmymoney/mymoney/mymoneytransactionfilter.h b/kmymoney/mymoney/mymoneytransactionfilter.h index c6e123ab3..a967a429d 100644 --- a/kmymoney/mymoney/mymoneytransactionfilter.h +++ b/kmymoney/mymoney/mymoneytransactionfilter.h @@ -1,577 +1,585 @@ /* * Copyright 2003-2019 Thomas Baumgart * Copyright 2004 Ace Jones * Copyright 2008-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYTRANSACTIONFILTER_H #define MYMONEYTRANSACTIONFILTER_H #include "kmm_mymoney_export.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class QString; class QDate; template class QList; class MyMoneyMoney; class MyMoneySplit; class MyMoneyAccount; namespace eMyMoney { namespace TransactionFilter { enum class Date; enum class Validity; } } /** * @author Thomas Baumgart * @author Łukasz Wojniłowicz */ class MyMoneyTransaction; class MyMoneyTransactionFilterPrivate; class KMM_MYMONEY_EXPORT MyMoneyTransactionFilter { Q_DECLARE_PRIVATE(MyMoneyTransactionFilter) protected: MyMoneyTransactionFilterPrivate* d_ptr; // name shouldn't colide with the one in mymoneyreport.h public: typedef union { unsigned allFilter; struct { unsigned textFilter : 1; unsigned accountFilter : 1; unsigned payeeFilter : 1; unsigned tagFilter : 1; unsigned categoryFilter : 1; unsigned nrFilter : 1; unsigned dateFilter : 1; unsigned amountFilter : 1; unsigned typeFilter : 1; unsigned stateFilter : 1; unsigned validityFilter : 1; } singleFilter; } FilterSet; /** * This is the standard constructor for a transaction filter. * It creates the object and calls setReportAllSplits() to * report all matching splits as separate entries. Use * setReportAllSplits() to override this behaviour. */ MyMoneyTransactionFilter(); /** * This is a convenience constructor to allow construction of * a simple account filter. It is basically the same as the * following: * * @code * : * MyMoneyTransactionFilter filter; * filter.setReportAllSplits(false); * filter.addAccount(id); * : * @endcode * * @param id reference to account id */ explicit MyMoneyTransactionFilter(const QString& id); MyMoneyTransactionFilter(const MyMoneyTransactionFilter & other); MyMoneyTransactionFilter(MyMoneyTransactionFilter && other); MyMoneyTransactionFilter & operator=(MyMoneyTransactionFilter other); friend void swap(MyMoneyTransactionFilter& first, MyMoneyTransactionFilter& second); virtual ~MyMoneyTransactionFilter(); /** * This method is used to clear the filter. All settings will be * removed. */ void clear(); /** * This method is used to clear the accounts filter only. */ void clearAccountFilter(); /** * This method is used to set the regular expression filter to the value specified * as parameter @p exp. The following text based fields are searched: * * - Memo * - Payee * - Tag * - Category * - Shares / Value * - Number * * @param exp The regular expression that must be found in a transaction * before it is included in the result set. * @param invert If true, value must not be contained in any of the above mentioned fields * */ void setTextFilter(const QRegExp& exp, bool invert = false); /** * This method will add the account with id @p id to the list of matching accounts. * If the list is empty, any transaction will match. * * @param id internal ID of the account */ void addAccount(const QString& id); /** * This is a convenience method and behaves exactly like the above * method but for a list of id's. */ void addAccount(const QStringList& ids); /** * This method will add the category with id @p id to the list of matching categories. * If the list is empty, only transaction with a single asset/liability account will match. * * @param id internal ID of the account */ void addCategory(const QString& id); /** * This is a convenience method and behaves exactly like the above * method but for a list of id's. */ void addCategory(const QStringList& ids); /** * This method sets the date filter to match only transactions with posting dates in * the date range specified by @p from and @p to. If @p from equal QDate() * all transactions with dates prior to @p to match. If @p to equals QDate() * all transactions with posting dates past @p from match. If @p from and @p to * are equal QDate() the filter is not activated and all transactions match. * * @param from from date * @param to to date */ void setDateFilter(const QDate& from, const QDate& to); void setDateFilter(eMyMoney::TransactionFilter::Date range); /** * This method sets the amount filter to match only transactions with * an amount in the range specified by @p from and @p to. * If a specific amount should be searched, @p from and @p to should be * the same value. * * @param from smallest value to match * @param to largest value to match */ void setAmountFilter(const MyMoneyMoney& from, const MyMoneyMoney& to); /** * This method will add the payee with id @p id to the list of matching payees. * If the list is empty, any transaction will match. * * @param id internal id of the payee */ void addPayee(const QString& id); /** * This method will add the tag with id @ta id to the list of matching tags. * If the list is empty, any transaction will match. * * @param id internal id of the tag */ void addTag(const QString& id); /** */ void addType(const int type); /** */ void addValidity(const int type); /** */ void addState(const int state); /** * This method sets the number filter to match only transactions with * a number in the range specified by @p from and @p to. * If a specific number should be searched, @p from and @p to should be * the same value. * * @param from smallest value to match * @param to largest value to match * * @note @p from and @p to can contain alphanumeric text */ void setNumberFilter(const QString& from, const QString& to); /** * This method is used to check a specific transaction against the filter. * The transaction will match the whole filter, if all specified filters * match. If the filter is cleared using the clear() method, any transaction * matches. Matching splits from the transaction are returned by @ref * matchingSplits(). * * @param transaction A transaction * * @retval true The transaction matches the filter set * @retval false The transaction does not match at least one of * the filters in the filter set */ bool match(const MyMoneyTransaction& transaction); /** * This method is used to check a specific split against the * text filter. The split will match if all specified and * checked filters match. If the filter is cleared using the clear() * method, any split matches. * * @param sp pointer to the split to be checked * * @retval true The split matches the filter set * @retval false The split does not match at least one of * the filters in the filter set */ bool matchText(const MyMoneySplit& s, const MyMoneyAccount &acc) const; /** * This method is used to check a specific split against the * amount filter. The split will match if all specified and * checked filters match. If the filter is cleared using the clear() * method, any split matches. * * @param sp const reference to the split to be checked * * @retval true The split matches the filter set * @retval false The split does not match at least one of * the filters in the filter set */ bool matchAmount(const MyMoneySplit& s) const; /** * Convenience method which actually returns matchText(sp) && matchAmount(sp). */ bool match(const MyMoneySplit& s) const; /** * This method is used to switch the amount of splits reported * by matchingSplits(). If the argument @p report is @p true (the default * if no argument specified) then matchingSplits() will return all * matching splits of the transaction. If @p report is set to @p false, * then only the very first matching split will be returned by * matchingSplits(). * * @param report controls the behaviour of matchingsSplits() as explained above. */ void setReportAllSplits(const bool report = true); void setConsiderCategory(const bool check = true); + void setTreatTransfersAsIncomeExpense(const bool check = true); + /** * This method is to avoid returning matching splits list * if only its count is needed * @return count of matching splits */ uint matchingSplitsCount(const MyMoneyTransaction& transaction); /** * This method returns a list of the matching splits for the filter. * If m_reportAllSplits is set to false, then only the very first * split will be returned. Use setReportAllSplits() to change the * behaviour. * * @return reference list of MyMoneySplit objects containing the * matching splits. If multiple splits match, only the first * one will be returned. * * @note an empty list will be returned, if the filter only required * to check the data contained in the MyMoneyTransaction * object (e.g. posting-date, state, etc.). * * @note The constructors set m_reportAllSplits differently. Please * see the documentation of the constructors MyMoneyTransactionFilter() * and MyMoneyTransactionFilter(const QString&) for details. */ QVector matchingSplits(const MyMoneyTransaction& transaction); /** * This method returns the from date set in the filter. If * no value has been set up for this filter, then QDate() is * returned. * * @return returns m_fromDate */ QDate fromDate() const; /** * This method returns the to date set in the filter. If * no value has been set up for this filter, then QDate() is * returned. * * @return returns m_toDate */ QDate toDate() const; /** * This method is used to return information about the * presence of a specific category in the category filter. * The category in question is included in the filter set, * if it has been set or no category filter is set. * * @param cat id of category in question * @return true if category is in filter set, false otherwise */ bool includesCategory(const QString& cat) const; /** * This method is used to return information about the * presence of a specific account in the account filter. * The account in question is included in the filter set, * if it has been set or no account filter is set. * * @param acc id of account in question * @return true if account is in filter set, false otherwise */ bool includesAccount(const QString& acc) const; /** * This method is used to return information about the * presence of a specific payee in the account filter. * The payee in question is included in the filter set, * if it has been set or no account filter is set. * * @param pye id of payee in question * @return true if payee is in filter set, false otherwise */ bool includesPayee(const QString& pye) const; /** * This method is used to return information about the * presence of a specific tag in the account filter. * The tag in question is included in the filter set, * if it has been set or no account filter is set. * * @param tag id of tag in question * @return true if tag is in filter set, false otherwise */ bool includesTag(const QString& tag) const; /** * This method is used to return information about the * presence of a date filter. * * @param from result value for the beginning of the date range * @param to result value for the end of the date range * @return true if a date filter is set */ bool dateFilter(QDate& from, QDate& to) const; /** * This method is used to return information about the * presence of an amount filter. * * @param from result value for the low end of the amount range * @param to result value for the high end of the amount range * @return true if an amount filter is set */ bool amountFilter(MyMoneyMoney& from, MyMoneyMoney& to) const; /** * This method is used to return information about the * presence of an number filter. * * @param from result value for the low end of the number range * @param to result value for the high end of the number range * @return true if a number filter is set */ bool numberFilter(QString& from, QString& to) const; /** * This method returns whether a payee filter has been set, * and if so, it returns all the payees set in the filter. * * @param list list to append payees into * @return return true if a payee filter has been set */ bool payees(QStringList& list) const; /** * This method returns whether a tag filter has been set, * and if so, it returns all the tags set in the filter. * * @param list list to append tags into * @return return true if a tag filter has been set */ bool tags(QStringList& list) const; /** * This method returns whether an account filter has been set, * and if so, it returns all the accounts set in the filter. * * @param list list to append accounts into * @return return true if an account filter has been set */ bool accounts(QStringList& list) const; /** * This method returns whether a category filter has been set, * and if so, it returns all the categories set in the filter. * * @param list list to append categories into * @return return true if a category filter has been set */ bool categories(QStringList& list) const; /** * This method returns whether a type filter has been set, * and if so, it returns the first type in the filter. * * @param i int to replace with first type filter, untouched otherwise * @return return true if a type filter has been set */ bool firstType(int& i) const; bool types(QList& list) const; /** * This method returns whether a state filter has been set, * and if so, it returns the first state in the filter. * * @param i reference to int to replace with first state filter, untouched otherwise * @return return true if a state filter has been set */ bool firstState(int& i) const; bool states(QList& list) const; /** * This method returns whether a validity filter has been set, * and if so, it returns the first validity in the filter. * * @param i reference to int to replace with first validity filter, untouched otherwise * @return return true if a validity filter has been set */ bool firstValidity(int& i) const; bool validities(QList& list) const; /** * This method returns whether a text filter has been set, * and if so, it returns the text filter. * * @param text regexp to replace with text filter, or blank if none set * @return return true if a text filter has been set */ bool textFilter(QRegExp& text) const; /** * This method returns whether the text filter should return * that DO NOT contain the text */ bool isInvertingText() const; + /** + * This method returns whether transfers should be treated as + * income/expense transactions or not + */ + bool treatTransfersAsIncomeExpense() const; + /** * This method translates a plain-language date range into QDate * start & end * * @param range Plain-language range of dates, e.g. 'CurrentYear' * @param start QDate will be set to corresponding to the first date in @p range * @param end QDate will be set to corresponding to the last date in @p range * @return return true if a range was successfully set, or false if @p range was invalid */ static bool translateDateRange(eMyMoney::TransactionFilter::Date range, QDate& start, QDate& end); static void setFiscalYearStart(int firstMonth, int firstDay); FilterSet filterSet() const; /** * This member removes all references to object identified by @p id. Used * to remove objects which are about to be removed from the engine. */ void removeReference(const QString& id); private: /** * This is a conversion tool from eMyMoney::Split::State * to MyMoneyTransactionFilter::stateE types * * @param split reference to split in question * * @return converted reconcile flag of the split passed as parameter */ int splitState(const MyMoneySplit& split) const; /** * This is a conversion tool from MyMoneySplit::action * to MyMoneyTransactionFilter::typeE types * * @param t reference to transaction * @param split reference to split in question * * @return converted action of the split passed as parameter */ int splitType(const MyMoneyTransaction& t, const MyMoneySplit& split, const MyMoneyAccount &acc) const; /** * This method checks if a transaction is valid or not. A transaction * is considered valid, if the sum of all splits is zero, invalid otherwise. * * @param transaction reference to transaction to be checked * @retval valid transaction is valid * @retval invalid transaction is invalid */ eMyMoney::TransactionFilter::Validity validTransaction(const MyMoneyTransaction& transaction) const; }; inline void swap(MyMoneyTransactionFilter& first, MyMoneyTransactionFilter& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyTransactionFilter::MyMoneyTransactionFilter(MyMoneyTransactionFilter && other) : MyMoneyTransactionFilter() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyTransactionFilter & MyMoneyTransactionFilter::operator=(MyMoneyTransactionFilter other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyTransactionFilter objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyTransactionFilter) #endif diff --git a/kmymoney/plugins/views/reports/core/querytable.cpp b/kmymoney/plugins/views/reports/core/querytable.cpp index 786dd6455..1c5cc77df 100644 --- a/kmymoney/plugins/views/reports/core/querytable.cpp +++ b/kmymoney/plugins/views/reports/core/querytable.cpp @@ -1,2002 +1,2008 @@ /* * Copyright 2005 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "cashflowlist.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "mymoneyenums.h" namespace reports { // **************************************************************************** // // QueryTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void QueryTable::init() { m_columns.clear(); m_group.clear(); m_subtotal.clear(); m_postcolumns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: constructAccountTable(); m_columns << ctAccount; break; case eMyMoney::Report::RowType::Account: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; case eMyMoney::Report::RowType::Payee: case eMyMoney::Report::RowType::Tag: case eMyMoney::Report::RowType::Month: case eMyMoney::Report::RowType::Week: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; case eMyMoney::Report::RowType::CashFlow: constructSplitsTable(); m_columns << ctPostDate; break; default: constructTransactionTable(); m_columns << ctPostDate; } // Sort the data to match the report definition m_subtotal << ctValue; switch (m_config.rowType()) { case eMyMoney::Report::RowType::CashFlow: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::Category: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::TopCategory: m_group << ctCategoryType << ctTopCategory; break; case eMyMoney::Report::RowType::TopAccount: m_group << ctTopAccount << ctAccount; break; case eMyMoney::Report::RowType::Account: m_group << ctAccount; break; case eMyMoney::Report::RowType::AccountReconcile: m_group << ctAccount << ctReconcileFlag; break; case eMyMoney::Report::RowType::Payee: m_group << ctPayee; break; case eMyMoney::Report::RowType::Tag: m_group << ctTag; break; case eMyMoney::Report::RowType::Month: m_group << ctMonth; break; case eMyMoney::Report::RowType::Week: m_group << ctWeek; break; case eMyMoney::Report::RowType::AccountByTopAccount: m_group << ctTopAccount; break; case eMyMoney::Report::RowType::EquityType: m_group << ctEquityType; break; case eMyMoney::Report::RowType::AccountType: m_group << ctType; break; case eMyMoney::Report::RowType::Institution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION_CSTRING("QueryTable::QueryTable(): unhandled row type"); } QVector sort = QVector::fromList(m_group) << QVector::fromList(m_columns) << ctID << ctRank; m_columns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); if (qc & eMyMoney::Report::QueryColumn::Number) m_columns << ctNumber; if (qc & eMyMoney::Report::QueryColumn::Payee) m_columns << ctPayee; if (qc & eMyMoney::Report::QueryColumn::Tag) m_columns << ctTag; if (qc & eMyMoney::Report::QueryColumn::Category) m_columns << ctCategory; if (qc & eMyMoney::Report::QueryColumn::Account) m_columns << ctAccount; if (qc & eMyMoney::Report::QueryColumn::Reconciled) m_columns << ctReconcileFlag; if (qc & eMyMoney::Report::QueryColumn::Memo) m_columns << ctMemo; if (qc & eMyMoney::Report::QueryColumn::Action) m_columns << ctAction; if (qc & eMyMoney::Report::QueryColumn::Shares) m_columns << ctShares; if (qc & eMyMoney::Report::QueryColumn::Price) m_columns << ctPrice; if (qc & eMyMoney::Report::QueryColumn::Performance) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Sold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Period: default: m_columns << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; } } if (qc & eMyMoney::Report::QueryColumn::CapitalGain) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; case eMyMoney::Report::InvestmentSum::Sold: default: m_columns << ctBuys << ctSells << ctCapitalGain; m_subtotal << ctBuys << ctSells << ctCapitalGain; if (m_config.isShowingSTLTCapitalGains()) { m_columns << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; m_subtotal << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; } break; } } if (qc & eMyMoney::Report::QueryColumn::Loan) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } if (qc & eMyMoney::Report::QueryColumn::Balance) m_postcolumns << ctBalance; TableRow::setSortCriteria(sort); qSort(m_rows); if (m_config.isShowingColumnTotals()) constructTotalRows(); // adds total rows to m_rows } void QueryTable::constructTotalRows() { if (m_rows.isEmpty()) return; // qSort places grand total at last position, because it doesn't belong to any group for (int i = 0; i < m_rows.count(); ++i) { if (m_rows.at(0)[ctRank] == QLatin1String("4") || m_rows.at(0)[ctRank] == QLatin1String("5")) // it should be unlikely that total row is at the top of rows, so... m_rows.move(0, m_rows.count() - 1 - i); // ...move it at the bottom else break; } MyMoneyFile* file = MyMoneyFile::instance(); QList subtotals = m_subtotal; QList groups = m_group; QList columns = m_columns; if (!m_subtotal.isEmpty() && subtotals.count() == 1) columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) columns.append(postcolumns); QMap>> totalCurrency; QList> totalGroups; QMap totalsValues; // initialize all total values under summed columns to be zero foreach (auto subtotal, subtotals) { totalsValues.insert(subtotal, MyMoneyMoney()); } totalsValues.insert(ctRowsCount, MyMoneyMoney()); // create total groups containing totals row for each group totalGroups.append(totalsValues); // prepend with extra group for grand total for (int j = 0; j < groups.count(); ++j) { totalGroups.append(totalsValues); } QList stashedTotalRows; int iCurrentRow, iNextRow; for (iCurrentRow = 0; iCurrentRow < m_rows.count();) { iNextRow = iCurrentRow + 1; // total rows are useless at summing so remove whole block of them at once while (iNextRow != m_rows.count() && (m_rows.at(iNextRow).value(ctRank) == QLatin1String("4") || m_rows.at(iNextRow).value(ctRank) == QLatin1String("5"))) { stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case } bool lastRow = (iNextRow == m_rows.count()); // sum all subtotal values for lowest group QString currencyID = m_rows.at(iCurrentRow).value(ctCurrency); if (m_rows.at(iCurrentRow).value(ctRank) == QLatin1String("1")) { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2) foreach (auto subtotal, subtotals) { if (!totalCurrency.contains(currencyID)) totalCurrency[currencyID].append(totalGroups); totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]); } totalCurrency[currencyID].last()[ctRowsCount] += MyMoneyMoney::ONE; } auto levelToClose = groups.count(); if (!lastRow) { for (int i = 0; i < groups.count(); ++i) { if (m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) { levelToClose = i; break; } } } else { levelToClose = 0; // all, we're done } // iterate over groups from the lowest to the highest to close groups for (int i = groups.count() - 1; i >= levelToClose ; --i) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { if (!MyMoneyMoney((*currencyGrp).at(i + 1).value(ctRowsCount)).isZero()) { // if no rows summed up, then no totals row TableRow totalsRow; // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values QMap::iterator upperGrp = (*currencyGrp)[i].begin(); QMap::iterator lowerGrp = (*currencyGrp)[i + 1].begin(); while(upperGrp != (*currencyGrp)[i].end()) { totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... (*upperGrp) += (*lowerGrp); // (*lowerGrp) = MyMoneyMoney(); ++upperGrp; ++lowerGrp; } // custom total values calculations foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value(ctBuys) - (*currencyGrp).at(i + 1).value(ctReinvestIncome), (*currencyGrp).at(i + 1).value(ctSells), (*currencyGrp).at(i + 1).value(ctStartingBalance), (*currencyGrp).at(i + 1).value(ctEndingBalance) + (*currencyGrp).at(i + 1).value(ctMarketValue), (*currencyGrp).at(i + 1).value(ctCashIncome)); else if (subtotal == ctPercentageGain) { const MyMoneyMoney denominator = (*currencyGrp).at(i + 1).value(ctBuys).abs(); totalsRow[subtotal] = denominator.isZero() ? QString(): (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / denominator).toString(); } else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value(ctPrice) / (*currencyGrp).at(i + 1).value(ctRowsCount)).toString(); } // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { if (stashedTotalRows.at(j).value(ctCurrency) != currencyID) continue; foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } break; } } (*currencyGrp).replace(i + 1, totalsValues); for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)]; // ...and identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString::number(i); totalsRow.remove(ctRowsCount); m_rows.insert(iNextRow++, totalsRow); // iCurrentRow and iNextRow can diverge here by more than one } ++currencyGrp; } } // code to put grand total row if (lastRow) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { TableRow totalsRow; QMap::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin(); while(grandTotalGrp != (*currencyGrp)[0].constEnd()) { totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); ++grandTotalGrp; } foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value(ctBuys) - (*currencyGrp).at(0).value(ctReinvestIncome), (*currencyGrp).at(0).value(ctSells), (*currencyGrp).at(0).value(ctStartingBalance), (*currencyGrp).at(0).value(ctEndingBalance) + (*currencyGrp).at(0).value(ctMarketValue), (*currencyGrp).at(0).value(ctCashIncome)); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(0).value(ctBuys) + (*currencyGrp).at(0).value(ctMarketValue)) / (*currencyGrp).at(0).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value(ctPrice) / (*currencyGrp).at(0).value(ctRowsCount)).toString(); } if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } } } for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = QString(); // no identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString(); m_rows.append(totalsRow); ++currencyGrp; } break; // no use to loop further } iCurrentRow = iNextRow; // iCurrent makes here a leap forward by at least one } } void QueryTable::constructTransactionTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); bool use_transfers; bool use_summary; bool hide_details; bool tag_special_case = false; switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: use_summary = false; - use_transfers = false; + use_transfers = report.isIncludingTransfers(); + report.setTreatTransfersAsIncomeExpense(use_transfers); hide_details = false; break; case eMyMoney::Report::RowType::Payee: use_summary = false; - use_transfers = false; + use_transfers = report.isIncludingTransfers(); + report.setTreatTransfersAsIncomeExpense(use_transfers); hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; case eMyMoney::Report::RowType::Tag: use_summary = false; - use_transfers = false; + use_transfers = report.isIncludingTransfers(); + report.setTreatTransfersAsIncomeExpense(use_transfers); hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { - myBegin = it_split; + // continue if split references an unselected account + if (report.includesAccount(splitAcc.id())) { + myBegin = it_split; + } } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } bool include_me = true; bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only QString a_fullname; QString a_memo; int pass = 1; QString myBeginCurrency; QString baseCurrency = file->baseCurrency().id(); QMap xrMap; // container for conversion rates from given currency to myBeginCurrency do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); QString splitCurrency; if (splitAcc.isInvest()) splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId(); else splitCurrency = file->account((*it_split).accountId()).currencyId(); if (it_split == myBegin) myBeginCurrency = splitCurrency; //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); //convert to base currency if (m_config.isConvertCurrency()) { xr = xrMap.value(splitCurrency, xr); // check if there is conversion rate to myBeginCurrency already stored... if (xr == MyMoneyMoney()) // ...if not... xr = (*it_split).price(); // ...take conversion rate to myBeginCurrency from split else if (splitAcc.isInvest()) // if it's stock split... xr *= (*it_split).price(); // ...multiply it by stock price stored in split if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (myBeginCurrency != baseCurrency) { // myBeginCurrency can differ from baseCurrency... MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); // ...so check conversion rate... if (price.isValid()) { xr *= price.rate(baseCurrency); // ...and multiply it by current price... qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; // ...and set information about non-baseCurrency } } else if (splitAcc.isInvest()) xr = (*it_split).price(); else xr = MyMoneyMoney::ONE; qA[ctTag].clear(); if (it_split == myBegin && splits.count() > 1) { include_me = m_config.includes(splitAcc); if (include_me) // track accts that will need opening and closing balances //FIXME in some cases it will show the opening and closing //balances but no transactions if the splits are all filtered out -- asoliverez accts.insert(splitAcc.id(), splitAcc); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : shares.toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && shares.isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!(assetAccountSplit == MyMoneySplit())) { for (it_split = splits.begin(); it_split != splits.end(); ++it_split) { if ((*it_split) == assetAccountSplit) { splitAcc = ReportAccount(assetAccountSplit.accountId()); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account myBegin = it_split; // set myBegin to asset split, so stock split can be listed in details under splits myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA[ctPrice] = shares.isZero() ? QString() : (stockSplit.price() * xr / (*it_split).price()).toString(); // put conversion rate for all splits with this currency, so... // every split of transaction have the same conversion rate xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price()); } else xr = (*it_split).price(); break; } } } } else qA[ctPrice] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match((*it_split)); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); qS[ctCategoryType] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); qA[ctSplit].clear(); } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctRank] = QLatin1Char('1'); qA[ctCategory] = i18n("[Split Transaction]"); qA[ctTopCategory] = i18nc("Split transaction", "Split"); qA[ctCategoryType] = i18nc("Split transaction", "Split"); m_rows += qA; } } } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) { // put the payment in the "payment" column and convert to lowest fraction qA[ctPayee] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { // put the interest in the "interest" column and convert to lowest fraction qA[ctInterest] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA[ctFees]); qA[ctFees] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA after all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA[ctValue].clear(); //convert to lowest fraction qA[ctSplit] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('2'); qA[ctTag] = ""; QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA[ctTag] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ", "; } } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: case eMyMoney::Report::RowType::Tag: case eMyMoney::Report::RowType::Payee: if (splitAcc.isIncomeExpense()) qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it break; default: break; } qA[ctSplit].clear(); qA[ctRank] = QLatin1Char('1'); } qA [ctMemo] = (*it_split).memo(); if (!m_containsNonBaseCurrency && splitAcc.currencyId() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qS[ctCurrency] = file->baseCurrency().id(); else qS[ctCurrency] = splitAcc.currency().id(); if (! splitAcc.isIncomeExpense()) { qA[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA[ctTopCategory] = splitAcc.topParentName(); qA[ctCategoryType] = i18n("Transfer"); } else { qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); } if (splits.count() > 1) { if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { if (tag_special_case) { if (tagIdListCache.isEmpty()) { qA[ctTag] = i18n("[No Tag]"); } else { QString delimiter; foreach(const auto tagId, tagIdListCache) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } } m_rows += qA; } } } } } if ((m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) || splits.count() == 1) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctRank] = QLatin1Char('1'); qS[ctAccount] = splitAcc.name(); qS[ctAccountID] = splitAcc.id(); qS[ctTopAccount] = splitAcc.topParentName(); if (splits.count() > 1) { qS[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); } else { qS[ctCategory] = i18n("*** UNASSIGNED ***"); } qS[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS[ctMemo] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? if (tagIdList.isEmpty()) { qS[ctTag] = i18n("[No Tag]"); } else { QString delimiter; foreach(const auto tagId, tagIdList) { qS[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qS[ctPayee] = payee.isEmpty() ? qA[ctPayee] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account(*it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); qA[ctPrice] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; //ending balance qA[ctPrice] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctRank] = QLatin1Char('3'); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } QString QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!(startingBal - buys).isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); return returnInvestment.convert(10000).toString(); } else return QString(); } QString QueryTable::helperIRR(const CashFlowList &all) const { try { return MyMoneyMoney(all.XIRR(), 10000).toString(); } catch (MyMoneyException &e) { qDebug() << e.what(); all.dumpDebug(); return QString(); } } void QueryTable::sumInvestmentValues(const ReportAccount& account, QList& cfList, QList& shList) const { for (int i = InvestmentValue::Buys; i < InvestmentValue::End; ++i) cfList.append(CashFlowList()); for (int i = InvestmentValue::Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList.append(MyMoneyMoney()); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; const bool isSTLT = report.isShowingSTLTCapitalGains(); const int settlementPeriod = report.settlementPeriod(); QDate termSeparator = report.termSeparator().addDays(-settlementPeriod); report.validDateRange(startingDate, endingDate); newStartingDate = startingDate; newEndingDate = endingDate; if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { // Saturday and Sunday aren't valid settlement dates if (endingDate.dayOfWeek() == Qt::Saturday) endingDate = endingDate.addDays(-1); else if (endingDate.dayOfWeek() == Qt::Sunday) endingDate = endingDate.addDays(-2); if (termSeparator.dayOfWeek() == Qt::Saturday) termSeparator = termSeparator.addDays(-1); else if (termSeparator.dayOfWeek() == Qt::Sunday) termSeparator = termSeparator.addDays(-2); if (startingDate.daysTo(endingDate) <= settlementPeriod) // no days to check for return; termSeparator = termSeparator.addDays(-settlementPeriod); newEndingDate = endingDate.addDays(-settlementPeriod); } shList[BuysOfOwned] = file->balance(account.id(), newEndingDate); // get how many shares there are at the end of period MyMoneyMoney stashedBuysOfOwned = shList.at(BuysOfOwned); bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); report.setDateFilter(newStartingDate, newEndingDate); do { QList transactions = file->transactionList(report); for (QList::const_reverse_iterator it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) { MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = (*it_t).postDate(); MyMoneyMoney price; //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::BuyShares) { if (reportedDateRange) { cfList[Buys].append(CashFlowListItem(postDate, value)); shList[Buys] += shares; } if (shList.at(BuysOfOwned).isZero()) { // add sold shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially sold shares MyMoneyMoney tempVal = (((shList.at(Sells).abs() - shList.at(BuysOfSells))) / shares) * value; cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] = shList.at(Sells).abs(); if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] = shList.at(BuysOfSells); } } else { // add wholly sold shares cfList[BuysOfSells].append(CashFlowListItem(postDate, value)); shList[BuysOfSells] += shares; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, value)); shList[LongTermBuysOfSells] += shares; } } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-sold shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-sold shares MyMoneyMoney tempVal = ((shares - shList.at(BuysOfOwned)) / shares) * value; MyMoneyMoney tempVal2 = (shares - shList.at(BuysOfOwned)); cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] += tempVal2; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] += tempVal2; } cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares && reportedDateRange) { cfList[Sells].append(CashFlowListItem(postDate, value)); shList[Sells] += shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares) { // shares variable is denominator of split ratio here for (int i = Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList[i] /= shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::AddShares || // added shares, when sold give 100% capital gain transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { if (shList.at(BuysOfOwned).isZero()) { // add added/reinvested shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially added/reinvested shares shList[BuysOfSells] = shList.at(Sells).abs(); if (postDate < termSeparator) shList[LongTermBuysOfSells] = shList[BuysOfSells]; } else { // add wholly added/reinvested shares shList[BuysOfSells] += shares; if (postDate < termSeparator) shList[LongTermBuysOfSells] += shares; } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-added/not-reinvested shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-added/not-reinvested shares MyMoneyMoney tempVal = (shares - shList.at(BuysOfOwned)); shList[BuysOfSells] += tempVal; if (postDate < termSeparator) shList[LongTermBuysOfSells] += tempVal; cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } if (transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { value = MyMoneyMoney(); foreach (const auto split, interestSplits) value += split.value(); value *= price; cfList[ReinvestIncome].append(CashFlowListItem(postDate, -value)); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares && reportedDateRange) // removed shares give no value in return so no capital gain on them shList[Sells] += shares; else if (transactionType == eMyMoney::Split::InvestmentTransactionType::Dividend || transactionType == eMyMoney::Split::InvestmentTransactionType::Yield) cfList[CashIncome].append(CashFlowListItem(postDate, value)); } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while ( ( (report.investmentSum() == eMyMoney::Report::InvestmentSum::Owned && !shList[BuysOfOwned].isZero()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::OwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()))) ) && account.openingDate() <= newEndingDate ); // we've got buy value and no sell value of long-term shares, so get them if (isSTLT && !shList[LongTermBuysOfSells].isZero()) { newStartingDate = startingDate; newEndingDate = endingDate.addDays(-settlementPeriod); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier QList transactions = file->transactionList(report); shList[BuysOfOwned] = shList[LongTermBuysOfSells]; foreach (const auto transaction, transactions) { MyMoneySplit shareSplit = transaction.splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = transaction.postDate(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { // add partially sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, (shList.at(LongTermSellsOfBuys).abs() - shList.at(LongTermBuysOfSells)) / shares * value)); shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else { // add wholly sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, value)); shList[LongTermSellsOfBuys] += shares; } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else shList[LongTermSellsOfBuys] += shares; } } } shList[BuysOfOwned] = stashedBuysOfOwned; report.setDateFilter(startingDate, endingDate); // reset data filter for next security return; } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); MyMoneyFile* file = MyMoneyFile::instance(); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); else price = account.deepCurrencyPrice(startingDate); MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal; MyMoneyMoney sellsTotal; MyMoneyMoney cashIncomeTotal; MyMoneyMoney reinvestIncomeTotal; switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: buysTotal = cfList.at(BuysOfSells).total() + cfList.at(BuysOfOwned).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(BuysOfOwned)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctEndingBalance] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Owned: buysTotal = cfList.at(BuysOfOwned).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && endingBal.isZero()) return; all.append(cfList.at(BuysOfOwned)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctMarketValue] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Sold: buysTotal = cfList.at(BuysOfSells).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); startingBal = endingBal = MyMoneyMoney(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); break; case eMyMoney::Report::InvestmentSum::Period: default: buysTotal = cfList.at(Buys).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero()) return; all.append(cfList.at(Buys)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); all.append(CashFlowListItem(startingDate, -startingBal)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctStartingBalance] = startingBal.toString(); result[ctEndingBalance] = endingBal.toString(); break; } result[ctBuys] = buysTotal.toString(); result[ctReturn] = helperIRR(all); result[ctReturnInvestment] = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal = cfList.at(BuysOfSells).total(); MyMoneyMoney sellsTotal = cfList.at(Sells).total(); MyMoneyMoney longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); MyMoneyMoney longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: { if (shList.at(BuysOfOwned).isZero()) return; MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = shList.at(BuysOfOwned) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); buysTotal = cfList.at(BuysOfOwned).total() - cfList.at(ReinvestIncome).total(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); result[ctBuys] = buysTotal.toString(); result[ctShares] = shList.at(BuysOfOwned).toString(); result[ctBuyPrice] = (buysTotal.abs() / shList.at(BuysOfOwned)).convertPrecision(pricePrecision).toString(); result[ctLastPrice] = price.toString(); result[ctMarketValue] = endingBal.toString(); result[ctCapitalGain] = (buysTotal + endingBal).toString(); result[ctPercentageGain] = buysTotal.isZero() ? QString() : ((buysTotal + endingBal)/buysTotal.abs()).toString(); break; } case eMyMoney::Report::InvestmentSum::Sold: default: buysTotal = cfList.at(BuysOfSells).total() - cfList.at(ReinvestIncome).total(); sellsTotal = cfList.at(Sells).total(); longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && longTermBuysOfSellsTotal.isZero() && longTermSellsOfBuys.isZero()) return; result[ctBuys] = buysTotal.toString(); result[ctSells] = sellsTotal.toString(); result[ctCapitalGain] = (buysTotal + sellsTotal).toString(); if (m_config.isShowingSTLTCapitalGains()) { result[ctBuysLT] = longTermBuysOfSellsTotal.toString(); result[ctSellsLT] = longTermSellsOfBuys.toString(); result[ctCapitalGainLT] = (longTermBuysOfSellsTotal + longTermSellsOfBuys).toString(); result[ctBuysST] = (buysTotal - longTermBuysOfSellsTotal).toString(); result[ctSellsST] = (sellsTotal - longTermSellsOfBuys).toString(); result[ctCapitalGainST] = ((buysTotal - longTermBuysOfSellsTotal) + (sellsTotal - longTermSellsOfBuys)).toString(); } break; } result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QMap> currencyCashFlow; // for total calculation QList accounts; file->accountList(accounts); for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) { // Note, "Investment" accounts are never included in account rows because // they don't contain anything by themselves. In reports, they are only // useful as a "topaccount" aggregator of stock accounts if ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != eMyMoney::Account::Type::Investment) { // don't add the account if it is closed. In fact, the business logic // should prevent that an account can be closed with a balance not equal // to zero, but we never know. MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate()); if (shares.isZero() && (*it_account).isClosed()) continue; ReportAccount account(*it_account); TableRow qaccountrow; CashFlowList accountCashflow; // for total calculation switch(m_config.queryColumns()) { case eMyMoney::Report::QueryColumn::Performance: { constructPerformanceRow(account, qaccountrow, accountCashflow); if (!qaccountrow.isEmpty()) { // assuming that that report is grouped by topaccount qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); if (!currencyCashFlow.value(qaccountrow.value(ctCurrency)).contains(qaccountrow.value(ctTopAccount))) currencyCashFlow[qaccountrow.value(ctCurrency)].insert(qaccountrow.value(ctTopAccount), accountCashflow); // create cashflow for unknown account... else currencyCashFlow[qaccountrow.value(ctCurrency)][qaccountrow.value(ctTopAccount)] += accountCashflow; // ...or add cashflow for known account } break; } case eMyMoney::Report::QueryColumn::CapitalGain: constructCapitalGainRow(account, qaccountrow); break; default: { //get fraction for account int fraction = account.currency().smallestAccountFraction() != -1 ? account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()); if (m_config.isConvertCurrency() && account.isForeignCurrency()) netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price netprice = netprice.reduce(); shares = shares.reduce(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qaccountrow[ctPrice] = netprice.convertPrecision(pricePrecision).toString(); qaccountrow[ctValue] = (netprice * shares).convert(fraction).toString(); qaccountrow[ctShares] = shares.toString(); QString iid = account.institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && !account.isTopLevel()) iid = account.topParent().institutionId(); if (iid.isEmpty()) qaccountrow[ctInstitution] = i18nc("No institution", "None"); else qaccountrow[ctInstitution] = file->institution(iid).name(); qaccountrow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); } } if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; qaccountrow[ctRank] = QLatin1Char('1'); qaccountrow[ctAccount] = account.name(); qaccountrow[ctAccountID] = account.id(); qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); m_rows.append(qaccountrow); } } if (m_config.queryColumns() == eMyMoney::Report::QueryColumn::Performance && m_config.isShowingColumnTotals()) { TableRow qtotalsrow; qtotalsrow[ctRank] = QLatin1Char('4'); // add identification of row as total QMap currencyGrandCashFlow; QMap>::iterator currencyAccGrp = currencyCashFlow.begin(); while (currencyAccGrp != currencyCashFlow.end()) { // convert map of top accounts with cashflows to TableRow for (QMap::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) { qtotalsrow[ctTopAccount] = topAccount.key(); qtotalsrow[ctReturn] = helperIRR(topAccount.value()); qtotalsrow[ctCurrency] = currencyAccGrp.key(); currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value(); // cumulative sum of cashflows of each topaccount m_rows.append(qtotalsrow); // rows aren't sorted yet, so no problem with adding them randomly at the end } ++currencyAccGrp; } QMap::iterator currencyGrp = currencyGrandCashFlow.begin(); qtotalsrow[ctTopAccount].clear(); // empty topaccount because it's grand cashflow while (currencyGrp != currencyGrandCashFlow.end()) { qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()); qtotalsrow[ctCurrency] = currencyGrp.key(); m_rows.append(qtotalsrow); ++currencyGrp; } } } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc((*myBegin).accountId()); bool include_me = true; QString a_fullname; QString a_memo; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : (*it_split).shares().toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && (*it_split).shares().isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctPrice] = xr.convertPrecision(pricePrecision).toString(); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA[ctAccount] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.constBegin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc((*tempSplit).accountId()); if (! tempSplitAcc.isIncomeExpense()) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA[ctAccount] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA[ctAccount] = myBeginAcc.fullName(); } //category data is always the one of the split qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account((* it_account)); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qA[ctPrice] = startPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; qA[ctRank] = QLatin1Char('3'); //ending balance qA[ctPrice] = endPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } } diff --git a/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp b/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp index 7553c6681..45fbb6042 100644 --- a/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp +++ b/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp @@ -1,673 +1,676 @@ /*************************************************************************** kreportconfigurationdlg.cpp - description ------------------- begin : Mon Jun 21 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017, 2018 by Łukasz Wojniłowicz 2018 by Michael Kiefer ***************************************************************************/ /*************************************************************************** * * * 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 "kreportconfigurationfilterdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ktransactionfilter.h" #include "kmymoneyaccountselector.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "daterangedlg.h" #include "reporttabimpl.h" #include "mymoneyenums.h" #include #include #include #include #include #include #include #include class KReportConfigurationFilterDlgPrivate { Q_DISABLE_COPY(KReportConfigurationFilterDlgPrivate) public: KReportConfigurationFilterDlgPrivate(KReportConfigurationFilterDlg *qq) : q_ptr(qq), ui(new Ui::KReportConfigurationFilterDlg), m_tabRowColPivot(nullptr), m_tabRowColQuery(nullptr), m_tabChart(nullptr), m_tabRange(nullptr), m_dateRange(nullptr) { } ~KReportConfigurationFilterDlgPrivate() { delete ui; } KReportConfigurationFilterDlg *q_ptr; Ui::KReportConfigurationFilterDlg *ui; QPointer m_tabGeneral; QPointer m_tabRowColPivot; QPointer m_tabRowColQuery; QPointer m_tabChart; QPointer m_tabRange; QPointer m_tabCapitalGain; QPointer m_tabPerformance; QPointer m_tabFilters; MyMoneyReport m_initialState; MyMoneyReport m_currentState; QVector m_budgets; DateRangeDlg *m_dateRange; }; KReportConfigurationFilterDlg::KReportConfigurationFilterDlg(MyMoneyReport report, QWidget *parent) : QDialog(parent), d_ptr(new KReportConfigurationFilterDlgPrivate(this)) { Q_D(KReportConfigurationFilterDlg); d->ui->setupUi(this); d->m_initialState = report; d->m_currentState = report; // // Rework labeling // setWindowTitle(i18n("Report Configuration")); // // Rework the buttons // // the Apply button is always enabled d->ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); d->ui->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip for report configuration apply button", "Apply the configuration changes to the report")); connect(d->ui->buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KReportConfigurationFilterDlg::slotSearch); connect(d->ui->buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &QDialog::close); connect(d->ui->buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &KReportConfigurationFilterDlg::slotReset); connect(d->ui->buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KReportConfigurationFilterDlg::slotShowHelp); // // Add new tabs // if (d->m_initialState.reportType() == eMyMoney::Report::ReportType::PivotTable) { // we will use date range together with data range d->m_tabFilters = new KTransactionFilter(this, (report.rowType() == eMyMoney::Report::RowType::Account), false); } else { d->m_tabFilters = new KTransactionFilter(this, (report.rowType() == eMyMoney::Report::RowType::Account)); d->m_dateRange = d->m_tabFilters->dateRange(); } d->ui->m_tabWidget->addTab(d->m_tabFilters, i18nc("Filters tab", "Filters")); d->m_tabGeneral = new ReportTabGeneral(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(0, d->m_tabGeneral, i18nc("General tab", "General")); if (d->m_initialState.reportType() == eMyMoney::Report::ReportType::PivotTable) { int tabNr = 1; if (!(d->m_initialState.isIncludingPrice() || d->m_initialState.isIncludingAveragePrice())) { d->m_tabRowColPivot = new ReportTabRowColPivot(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabRowColPivot, i18n("Rows/Columns")); connect(d->m_tabRowColPivot->ui->m_comboRows, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotRowTypeChanged)); connect(d->m_tabRowColPivot->ui->m_comboRows, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotUpdateColumnsCombo)); //control the state of the includeTransfer check connect(d->m_tabFilters->categoriesView(), &KMyMoneySelector::stateChanged, this, &KReportConfigurationFilterDlg::slotUpdateCheckTransfers); } d->m_tabChart = new ReportTabChart(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabChart, i18n("Chart")); d->m_tabRange = new ReportTabRange(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabRange, i18n("Range")); d->m_dateRange = d->m_tabRange->m_dateRange; if (!(d->m_initialState.isIncludingPrice() || d->m_initialState.isIncludingAveragePrice())) { connect(d->m_tabRange->ui->m_comboColumns, static_cast(&QComboBox::activated), this, &KReportConfigurationFilterDlg::slotColumnTypeChanged); connect(d->m_tabRange->ui->m_comboColumns, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotUpdateColumnsCombo)); } connect(d->m_tabChart->ui->m_logYaxis, &QCheckBox::stateChanged, this, &KReportConfigurationFilterDlg::slotLogAxisChanged); connect(d->m_tabChart->ui->m_negExpenses, &QCheckBox::stateChanged, this, &KReportConfigurationFilterDlg::slotNegExpensesChanged); } else if (d->m_initialState.reportType() == eMyMoney::Report::ReportType::QueryTable) { // eInvestmentHoldings is a special-case report, and you cannot configure the // rows & columns of that report. if (d->m_initialState.rowType() < eMyMoney::Report::RowType::AccountByTopAccount) { d->m_tabRowColQuery = new ReportTabRowColQuery(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabRowColQuery, i18n("Rows/Columns")); } if (d->m_initialState.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { d->m_tabCapitalGain = new ReportTabCapitalGain(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabCapitalGain, i18n("Report")); } if (d->m_initialState.queryColumns() & eMyMoney::Report::QueryColumn::Performance) { d->m_tabPerformance = new ReportTabPerformance(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabPerformance, i18n("Report")); } } d->ui->m_criteriaTab->setCurrentIndex(d->ui->m_criteriaTab->indexOf(d->m_tabGeneral)); d->ui->m_criteriaTab->setMinimumSize(500, 200); QList list = MyMoneyFile::instance()->budgetList(); QList::const_iterator it_b; for (it_b = list.constBegin(); it_b != list.constEnd(); ++it_b) { d->m_budgets.push_back(*it_b); } // // Now set up the widgets with proper values // slotReset(); } KReportConfigurationFilterDlg::~KReportConfigurationFilterDlg() { } MyMoneyReport KReportConfigurationFilterDlg::getConfig() const { Q_D(const KReportConfigurationFilterDlg); return d->m_currentState; } void KReportConfigurationFilterDlg::slotSearch() { Q_D(KReportConfigurationFilterDlg); // setup the filter from the dialog widgets auto filter = d->m_tabFilters->setupFilter(); // Copy the m_filter over to the filter part of m_currentConfig. d->m_currentState.assignFilter(filter); // Then extract the report properties d->m_currentState.setName(d->m_tabGeneral->ui->m_editName->text()); d->m_currentState.setComment(d->m_tabGeneral->ui->m_editComment->text()); d->m_currentState.setConvertCurrency(d->m_tabGeneral->ui->m_checkCurrency->isChecked()); d->m_currentState.setFavorite(d->m_tabGeneral->ui->m_checkFavorite->isChecked()); d->m_currentState.setSkipZero(d->m_tabGeneral->ui->m_skipZero->isChecked()); if (d->m_tabRowColPivot) { eMyMoney::Report::DetailLevel dl[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; d->m_currentState.setDetailLevel(dl[d->m_tabRowColPivot->ui->m_comboDetail->currentIndex()]); // modify the rowtype only if the widget is enabled if (d->m_tabRowColPivot->ui->m_comboRows->isEnabled()) { eMyMoney::Report::RowType rt[2] = { eMyMoney::Report::RowType::ExpenseIncome, eMyMoney::Report::RowType::AssetLiability }; d->m_currentState.setRowType(rt[d->m_tabRowColPivot->ui->m_comboRows->currentIndex()]); } d->m_currentState.setShowingRowTotals(false); if (d->m_tabRowColPivot->ui->m_comboRows->currentIndex() == 0) d->m_currentState.setShowingRowTotals(d->m_tabRowColPivot->ui->m_checkTotalColumn->isChecked()); d->m_currentState.setShowingColumnTotals(d->m_tabRowColPivot->ui->m_checkTotalRow->isChecked()); d->m_currentState.setIncludingSchedules(d->m_tabRowColPivot->ui->m_checkScheduled->isChecked()); d->m_currentState.setIncludingTransfers(d->m_tabRowColPivot->ui->m_checkTransfers->isChecked()); d->m_currentState.setIncludingUnusedAccounts(d->m_tabRowColPivot->ui->m_checkUnused->isChecked()); if (d->m_tabRowColPivot->ui->m_comboBudget->isEnabled()) { d->m_currentState.setBudget(d->m_budgets[d->m_tabRowColPivot->ui->m_comboBudget->currentItem()].id(), d->m_initialState.rowType() == eMyMoney::Report::RowType::BudgetActual); } else { d->m_currentState.setBudget(QString(), false); } //set moving average days if (d->m_tabRowColPivot->ui->m_movingAverageDays->isEnabled()) { d->m_currentState.setMovingAverageDays(d->m_tabRowColPivot->ui->m_movingAverageDays->value()); } } else if (d->m_tabRowColQuery) { eMyMoney::Report::RowType rtq[8] = { eMyMoney::Report::RowType::Category, eMyMoney::Report::RowType::TopCategory, eMyMoney::Report::RowType::Tag, eMyMoney::Report::RowType::Payee, eMyMoney::Report::RowType::Account, eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::RowType::Month, eMyMoney::Report::RowType::Week }; d->m_currentState.setRowType(rtq[d->m_tabRowColQuery->ui->m_comboOrganizeBy->currentIndex()]); unsigned qc = eMyMoney::Report::QueryColumn::None; if (d->m_currentState.queryColumns() & eMyMoney::Report::QueryColumn::Loan) // once a loan report, always a loan report qc = eMyMoney::Report::QueryColumn::Loan; if (d->m_tabRowColQuery->ui->m_checkNumber->isChecked()) qc |= eMyMoney::Report::QueryColumn::Number; if (d->m_tabRowColQuery->ui->m_checkPayee->isChecked()) qc |= eMyMoney::Report::QueryColumn::Payee; if (d->m_tabRowColQuery->ui->m_checkTag->isChecked()) qc |= eMyMoney::Report::QueryColumn::Tag; if (d->m_tabRowColQuery->ui->m_checkCategory->isChecked()) qc |= eMyMoney::Report::QueryColumn::Category; if (d->m_tabRowColQuery->ui->m_checkMemo->isChecked()) qc |= eMyMoney::Report::QueryColumn::Memo; if (d->m_tabRowColQuery->ui->m_checkAccount->isChecked()) qc |= eMyMoney::Report::QueryColumn::Account; if (d->m_tabRowColQuery->ui->m_checkReconciled->isChecked()) qc |= eMyMoney::Report::QueryColumn::Reconciled; if (d->m_tabRowColQuery->ui->m_checkAction->isChecked()) qc |= eMyMoney::Report::QueryColumn::Action; if (d->m_tabRowColQuery->ui->m_checkShares->isChecked()) qc |= eMyMoney::Report::QueryColumn::Shares; if (d->m_tabRowColQuery->ui->m_checkPrice->isChecked()) qc |= eMyMoney::Report::QueryColumn::Price; if (d->m_tabRowColQuery->ui->m_checkBalance->isChecked()) qc |= eMyMoney::Report::QueryColumn::Balance; d->m_currentState.setQueryColumns(static_cast(qc)); d->m_currentState.setTax(d->m_tabRowColQuery->ui->m_checkTax->isChecked()); d->m_currentState.setInvestmentsOnly(d->m_tabRowColQuery->ui->m_checkInvestments->isChecked()); d->m_currentState.setLoansOnly(d->m_tabRowColQuery->ui->m_checkLoans->isChecked()); d->m_currentState.setDetailLevel(d->m_tabRowColQuery->ui->m_checkHideSplitDetails->isChecked() ? eMyMoney::Report::DetailLevel::None : eMyMoney::Report::DetailLevel::All); d->m_currentState.setHideTransactions(d->m_tabRowColQuery->ui->m_checkHideTransactions->isChecked()); d->m_currentState.setShowingColumnTotals(!d->m_tabRowColQuery->ui->m_checkHideTotals->isChecked()); + + d->m_currentState.setIncludingTransfers(d->m_tabRowColQuery->ui->m_checkTransfers->isChecked()); } if (d->m_tabChart) { eMyMoney::Report::ChartType ct[5] = { eMyMoney::Report::ChartType::Line, eMyMoney::Report::ChartType::Bar, eMyMoney::Report::ChartType::StackedBar, eMyMoney::Report::ChartType::Pie, eMyMoney::Report::ChartType::Ring }; d->m_currentState.setChartType(ct[d->m_tabChart->ui->m_comboType->currentIndex()]); d->m_currentState.setChartCHGridLines(d->m_tabChart->ui->m_checkCHGridLines->isChecked()); d->m_currentState.setChartSVGridLines(d->m_tabChart->ui->m_checkSVGridLines->isChecked()); d->m_currentState.setChartDataLabels(d->m_tabChart->ui->m_checkValues->isChecked()); d->m_currentState.setChartByDefault(d->m_tabChart->ui->m_checkShowChart->isChecked()); d->m_currentState.setChartLineWidth(d->m_tabChart->ui->m_lineWidth->value()); d->m_currentState.setLogYAxis(d->m_tabChart->ui->m_logYaxis->isChecked()); d->m_currentState.setNegExpenses(d->m_tabChart->ui->m_negExpenses->isChecked()); } if (d->m_tabRange) { d->m_currentState.setDataRangeStart(d->m_tabRange->ui->m_dataRangeStart->text()); d->m_currentState.setDataRangeEnd(d->m_tabRange->ui->m_dataRangeEnd->text()); d->m_currentState.setDataMajorTick(d->m_tabRange->ui->m_dataMajorTick->text()); d->m_currentState.setDataMinorTick(d->m_tabRange->ui->m_dataMinorTick->text()); d->m_currentState.setYLabelsPrecision(d->m_tabRange->ui->m_yLabelsPrecision->value()); d->m_currentState.setDataFilter((eMyMoney::Report::DataLock)d->m_tabRange->ui->m_dataLock->currentIndex()); eMyMoney::Report::ColumnType ct[6] = { eMyMoney::Report::ColumnType::Days, eMyMoney::Report::ColumnType::Weeks, eMyMoney::Report::ColumnType::Months, eMyMoney::Report::ColumnType::BiMonths, eMyMoney::Report::ColumnType::Quarters, eMyMoney::Report::ColumnType::Years }; bool dy[6] = { true, true, false, false, false, false }; d->m_currentState.setColumnType(ct[d->m_tabRange->ui->m_comboColumns->currentIndex()]); //TODO (Ace) This should be implicit in the call above. MMReport needs fixin' d->m_currentState.setColumnsAreDays(dy[d->m_tabRange->ui->m_comboColumns->currentIndex()]); d->m_currentState.setDateFilter(d->m_dateRange->fromDate(), d->m_dateRange->toDate()); } // setup the date lock eMyMoney::TransactionFilter::Date range = d->m_dateRange->dateRange(); d->m_currentState.setDateFilter(range); if (d->m_tabCapitalGain) { d->m_currentState.setTermSeparator(d->m_tabCapitalGain->ui->m_termSeparator->date()); d->m_currentState.setShowSTLTCapitalGains(d->m_tabCapitalGain->ui->m_showSTLTCapitalGains->isChecked()); d->m_currentState.setSettlementPeriod(d->m_tabCapitalGain->ui->m_settlementPeriod->value()); d->m_currentState.setShowingColumnTotals(!d->m_tabCapitalGain->ui->m_checkHideTotals->isChecked()); d->m_currentState.setInvestmentSum(static_cast(d->m_tabCapitalGain->ui->m_investmentSum->currentData().toInt())); } if (d->m_tabPerformance) { d->m_currentState.setShowingColumnTotals(!d->m_tabPerformance->ui->m_checkHideTotals->isChecked()); d->m_currentState.setInvestmentSum(static_cast(d->m_tabPerformance->ui->m_investmentSum->currentData().toInt())); } done(true); } void KReportConfigurationFilterDlg::slotRowTypeChanged(int row) { Q_D(KReportConfigurationFilterDlg); d->m_tabRowColPivot->ui->m_checkTotalColumn->setEnabled(row == 0); } void KReportConfigurationFilterDlg::slotColumnTypeChanged(int row) { Q_D(KReportConfigurationFilterDlg); if ((d->m_tabRowColPivot->ui->m_comboBudget->isEnabled() && row < 2)) { d->m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo() { Q_D(KReportConfigurationFilterDlg); const int monthlyIndex = 2; const int incomeExpenseIndex = 0; const bool isIncomeExpenseForecast = d->m_currentState.isIncludingForecast() && d->m_tabRowColPivot->ui->m_comboRows->currentIndex() == incomeExpenseIndex; if (isIncomeExpenseForecast && d->m_tabRange->ui->m_comboColumns->currentIndex() != monthlyIndex) { d->m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo(int) { slotUpdateColumnsCombo(); } void KReportConfigurationFilterDlg::slotLogAxisChanged(int state) { Q_D(KReportConfigurationFilterDlg); if (state == Qt::Checked) d->m_tabRange->setRangeLogarythmic(true); else d->m_tabRange->setRangeLogarythmic(false); } void KReportConfigurationFilterDlg::slotNegExpensesChanged(int state) { Q_D(KReportConfigurationFilterDlg); d->m_tabChart->setNegExpenses(state == Qt::Checked); } void KReportConfigurationFilterDlg::slotReset() { Q_D(KReportConfigurationFilterDlg); // // Set up the widget from the initial filter // d->m_currentState = d->m_initialState; // // Report Properties // d->m_tabGeneral->ui->m_editName->setText(d->m_initialState.name()); d->m_tabGeneral->ui->m_editComment->setText(d->m_initialState.comment()); d->m_tabGeneral->ui->m_checkCurrency->setChecked(d->m_initialState.isConvertCurrency()); d->m_tabGeneral->ui->m_checkFavorite->setChecked(d->m_initialState.isFavorite()); if (d->m_initialState.isIncludingPrice() || d->m_initialState.isSkippingZero()) { d->m_tabGeneral->ui->m_skipZero->setChecked(d->m_initialState.isSkippingZero()); } else { d->m_tabGeneral->ui->m_skipZero->setEnabled(false); } if (d->m_tabRowColPivot) { KComboBox *combo = d->m_tabRowColPivot->ui->m_comboDetail; switch (d->m_initialState.detailLevel()) { case eMyMoney::Report::DetailLevel::None: case eMyMoney::Report::DetailLevel::End: case eMyMoney::Report::DetailLevel::All: combo->setCurrentItem(i18nc("All accounts", "All"), false); break; case eMyMoney::Report::DetailLevel::Top: combo->setCurrentItem(i18n("Top-Level"), false); break; case eMyMoney::Report::DetailLevel::Group: combo->setCurrentItem(i18n("Groups"), false); break; case eMyMoney::Report::DetailLevel::Total: combo->setCurrentItem(i18n("Totals"), false); break; } combo = d->m_tabRowColPivot->ui->m_comboRows; switch (d->m_initialState.rowType()) { case eMyMoney::Report::RowType::ExpenseIncome: case eMyMoney::Report::RowType::Budget: case eMyMoney::Report::RowType::BudgetActual: combo->setCurrentItem(i18n("Income & Expenses"), false); // income / expense break; default: combo->setCurrentItem(i18n("Assets & Liabilities"), false); // asset / liability break; } d->m_tabRowColPivot->ui->m_checkTotalColumn->setChecked(d->m_initialState.isShowingRowTotals()); d->m_tabRowColPivot->ui->m_checkTotalRow->setChecked(d->m_initialState.isShowingColumnTotals()); slotRowTypeChanged(combo->currentIndex()); //load budgets combo if (d->m_initialState.rowType() == eMyMoney::Report::RowType::Budget || d->m_initialState.rowType() == eMyMoney::Report::RowType::BudgetActual) { d->m_tabRowColPivot->ui->m_comboRows->setEnabled(false); d->m_tabRowColPivot->ui->m_rowsLabel->setEnabled(false); d->m_tabRowColPivot->ui->m_budgetFrame->setEnabled(!d->m_budgets.empty()); auto i = 0; for (QVector::const_iterator it_b = d->m_budgets.constBegin(); it_b != d->m_budgets.constEnd(); ++it_b) { d->m_tabRowColPivot->ui->m_comboBudget->insertItem((*it_b).name(), i); //set the current selected item if ((d->m_initialState.budget() == "Any" && (*it_b).budgetStart().year() == QDate::currentDate().year()) || d->m_initialState.budget() == (*it_b).id()) d->m_tabRowColPivot->ui->m_comboBudget->setCurrentItem(i); i++; } } //set moving average days spinbox QSpinBox *spinbox = d->m_tabRowColPivot->ui->m_movingAverageDays; spinbox->setEnabled(d->m_initialState.isIncludingMovingAverage()); d->m_tabRowColPivot->ui->m_movingAverageLabel->setEnabled(d->m_initialState.isIncludingMovingAverage()); if (d->m_initialState.isIncludingMovingAverage()) { spinbox->setValue(d->m_initialState.movingAverageDays()); } d->m_tabRowColPivot->ui->m_checkScheduled->setChecked(d->m_initialState.isIncludingSchedules()); d->m_tabRowColPivot->ui->m_checkTransfers->setChecked(d->m_initialState.isIncludingTransfers()); d->m_tabRowColPivot->ui->m_checkUnused->setChecked(d->m_initialState.isIncludingUnusedAccounts()); } else if (d->m_tabRowColQuery) { KComboBox *combo = d->m_tabRowColQuery->ui->m_comboOrganizeBy; switch (d->m_initialState.rowType()) { case eMyMoney::Report::RowType::NoRows: case eMyMoney::Report::RowType::Category: combo->setCurrentItem(i18n("Categories"), false); break; case eMyMoney::Report::RowType::TopCategory: combo->setCurrentItem(i18n("Top Categories"), false); break; case eMyMoney::Report::RowType::Tag: combo->setCurrentItem(i18n("Tags"), false); break; case eMyMoney::Report::RowType::Payee: combo->setCurrentItem(i18n("Payees"), false); break; case eMyMoney::Report::RowType::Account: combo->setCurrentItem(i18n("Accounts"), false); break; case eMyMoney::Report::RowType::TopAccount: combo->setCurrentItem(i18n("Top Accounts"), false); break; case eMyMoney::Report::RowType::Month: combo->setCurrentItem(i18n("Month"), false); break; case eMyMoney::Report::RowType::Week: combo->setCurrentItem(i18n("Week"), false); break; default: throw MYMONEYEXCEPTION_CSTRING("KReportConfigurationFilterDlg::slotReset(): QueryTable report has invalid rowtype"); } unsigned qc = d->m_initialState.queryColumns(); d->m_tabRowColQuery->ui->m_checkNumber->setChecked(qc & eMyMoney::Report::QueryColumn::Number); d->m_tabRowColQuery->ui->m_checkPayee->setChecked(qc & eMyMoney::Report::QueryColumn::Payee); d->m_tabRowColQuery->ui->m_checkTag->setChecked(qc & eMyMoney::Report::QueryColumn::Tag); d->m_tabRowColQuery->ui->m_checkCategory->setChecked(qc & eMyMoney::Report::QueryColumn::Category); d->m_tabRowColQuery->ui->m_checkMemo->setChecked(qc & eMyMoney::Report::QueryColumn::Memo); d->m_tabRowColQuery->ui->m_checkAccount->setChecked(qc & eMyMoney::Report::QueryColumn::Account); d->m_tabRowColQuery->ui->m_checkReconciled->setChecked(qc & eMyMoney::Report::QueryColumn::Reconciled); d->m_tabRowColQuery->ui->m_checkAction->setChecked(qc & eMyMoney::Report::QueryColumn::Action); d->m_tabRowColQuery->ui->m_checkShares->setChecked(qc & eMyMoney::Report::QueryColumn::Shares); d->m_tabRowColQuery->ui->m_checkPrice->setChecked(qc & eMyMoney::Report::QueryColumn::Price); d->m_tabRowColQuery->ui->m_checkBalance->setChecked(qc & eMyMoney::Report::QueryColumn::Balance); d->m_tabRowColQuery->ui->m_checkTax->setChecked(d->m_initialState.isTax()); d->m_tabRowColQuery->ui->m_checkInvestments->setChecked(d->m_initialState.isInvestmentsOnly()); d->m_tabRowColQuery->ui->m_checkLoans->setChecked(d->m_initialState.isLoansOnly()); d->m_tabRowColQuery->ui->m_checkHideTransactions->setChecked(d->m_initialState.isHideTransactions()); d->m_tabRowColQuery->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabRowColQuery->ui->m_checkHideSplitDetails->setEnabled(!d->m_initialState.isHideTransactions()); d->m_tabRowColQuery->ui->m_checkHideSplitDetails->setChecked (d->m_initialState.detailLevel() == eMyMoney::Report::DetailLevel::None || d->m_initialState.isHideTransactions()); + d->m_tabRowColQuery->ui->m_checkTransfers->setChecked(d->m_initialState.isIncludingTransfers()); } if (d->m_tabChart) { KMyMoneyGeneralCombo* combo = d->m_tabChart->ui->m_comboType; switch (d->m_initialState.chartType()) { case eMyMoney::Report::ChartType::None: combo->setCurrentItem(static_cast(eMyMoney::Report::ChartType::Line)); break; case eMyMoney::Report::ChartType::Line: case eMyMoney::Report::ChartType::Bar: case eMyMoney::Report::ChartType::StackedBar: case eMyMoney::Report::ChartType::Pie: case eMyMoney::Report::ChartType::Ring: combo->setCurrentItem(static_cast(d->m_initialState.chartType())); break; default: throw MYMONEYEXCEPTION_CSTRING("KReportConfigurationFilterDlg::slotReset(): Report has invalid charttype"); } d->m_tabChart->ui->m_checkCHGridLines->setChecked(d->m_initialState.isChartCHGridLines()); d->m_tabChart->ui->m_checkSVGridLines->setChecked(d->m_initialState.isChartSVGridLines()); d->m_tabChart->ui->m_checkValues->setChecked(d->m_initialState.isChartDataLabels()); d->m_tabChart->ui->m_checkShowChart->setChecked(d->m_initialState.isChartByDefault()); d->m_tabChart->ui->m_lineWidth->setValue(d->m_initialState.chartLineWidth()); d->m_tabChart->ui->m_logYaxis->setChecked(d->m_initialState.isLogYAxis()); d->m_tabChart->ui->m_negExpenses->setChecked(d->m_initialState.isNegExpenses()); } if (d->m_tabRange) { d->m_tabRange->ui->m_dataRangeStart->setText(d->m_initialState.dataRangeStart()); d->m_tabRange->ui->m_dataRangeEnd->setText(d->m_initialState.dataRangeEnd()); d->m_tabRange->ui->m_dataMajorTick->setText(d->m_initialState.dataMajorTick()); d->m_tabRange->ui->m_dataMinorTick->setText(d->m_initialState.dataMinorTick()); d->m_tabRange->ui->m_yLabelsPrecision->setValue(d->m_initialState.yLabelsPrecision()); d->m_tabRange->ui->m_dataLock->setCurrentIndex((int)d->m_initialState.dataFilter()); KComboBox *combo = d->m_tabRange->ui->m_comboColumns; if (d->m_initialState.isColumnsAreDays()) { switch (d->m_initialState.columnType()) { case eMyMoney::Report::ColumnType::NoColumns: case eMyMoney::Report::ColumnType::Days: combo->setCurrentItem(i18nc("@item the columns will display daily data", "Daily"), false); break; case eMyMoney::Report::ColumnType::Weeks: combo->setCurrentItem(i18nc("@item the columns will display weekly data", "Weekly"), false); break; default: break; } } else { switch (d->m_initialState.columnType()) { case eMyMoney::Report::ColumnType::NoColumns: case eMyMoney::Report::ColumnType::Months: combo->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); break; case eMyMoney::Report::ColumnType::BiMonths: combo->setCurrentItem(i18nc("@item the columns will display bi-monthly data", "Bi-Monthly"), false); break; case eMyMoney::Report::ColumnType::Quarters: combo->setCurrentItem(i18nc("@item the columns will display quarterly data", "Quarterly"), false); break; case eMyMoney::Report::ColumnType::Years: combo->setCurrentItem(i18nc("@item the columns will display yearly data", "Yearly"), false); break; default: break; } } } if (d->m_tabCapitalGain) { d->m_tabCapitalGain->ui->m_termSeparator->setDate(d->m_initialState.termSeparator()); d->m_tabCapitalGain->ui->m_showSTLTCapitalGains->setChecked(d->m_initialState.isShowingSTLTCapitalGains()); d->m_tabCapitalGain->ui->m_settlementPeriod->setValue(d->m_initialState.settlementPeriod()); d->m_tabCapitalGain->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabCapitalGain->ui->m_investmentSum->blockSignals(true); d->m_tabCapitalGain->ui->m_investmentSum->clear(); d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only owned"), static_cast(eMyMoney::Report::InvestmentSum::Owned)); d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only sold"), static_cast(eMyMoney::Report::InvestmentSum::Sold)); d->m_tabCapitalGain->ui->m_investmentSum->blockSignals(false); d->m_tabCapitalGain->ui->m_investmentSum->setCurrentIndex(d->m_tabCapitalGain->ui->m_investmentSum->findData(static_cast(d->m_initialState.investmentSum()))); } if (d->m_tabPerformance) { d->m_tabPerformance->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabPerformance->ui->m_investmentSum->blockSignals(true); d->m_tabPerformance->ui->m_investmentSum->clear(); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("From period"), static_cast(eMyMoney::Report::InvestmentSum::Period)); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Owned and sold"), static_cast(eMyMoney::Report::InvestmentSum::OwnedAndSold)); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only owned"), static_cast(eMyMoney::Report::InvestmentSum::Owned)); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only sold"), static_cast(eMyMoney::Report::InvestmentSum::Sold)); d->m_tabPerformance->ui->m_investmentSum->blockSignals(false); d->m_tabPerformance->ui->m_investmentSum->setCurrentIndex(d->m_tabPerformance->ui->m_investmentSum->findData(static_cast(d->m_initialState.investmentSum()))); } d->m_tabFilters->resetFilter(d->m_initialState); if (d->m_dateRange) { d->m_initialState.updateDateFilter(); QDate dateFrom, dateTo; if (d->m_initialState.dateFilter(dateFrom, dateTo)) { if (d->m_initialState.isDateUserDefined()) { d->m_dateRange->setDateRange(dateFrom, dateTo); } else { d->m_dateRange->setDateRange(d->m_initialState.dateRange()); } } else { d->m_dateRange->setDateRange(eMyMoney::TransactionFilter::Date::All); } } } void KReportConfigurationFilterDlg::slotShowHelp() { Q_D(KReportConfigurationFilterDlg); if (d->ui->m_tabWidget->currentIndex() == 1) d->m_tabFilters->slotShowHelp(); else KHelpClient::invokeHelp("details.reports.config"); } //TODO Fix the reports and engine to include transfers even if categories are filtered - bug #1523508 void KReportConfigurationFilterDlg::slotUpdateCheckTransfers() { Q_D(KReportConfigurationFilterDlg); auto cb = d->m_tabRowColPivot->ui->m_checkTransfers; if (!d->m_tabFilters->categoriesView()->allItemsSelected()) { cb->setChecked(false); cb->setDisabled(true); } else { cb->setEnabled(true); } } diff --git a/kmymoney/plugins/views/reports/reporttabrowcolquery.ui b/kmymoney/plugins/views/reports/reporttabrowcolquery.ui index 258678c0d..d0a961b5b 100644 --- a/kmymoney/plugins/views/reports/reporttabrowcolquery.ui +++ b/kmymoney/plugins/views/reports/reporttabrowcolquery.ui @@ -1,547 +1,554 @@ ReportTabRowColQuery 0 0 730 267 0 0 Rows/Columns Tab <p>On this tab, you configure how you would like the rows and columns to be selected and organized.</p> 0 0 Organize by: false true 0 0 <p>Choose how to group the transactions in this report</p> Categories Top Categories Tags Payees Accounts Top Accounts Month Week Qt::Horizontal QSizePolicy::Expanding 358 16 true 0 0 <p>Choose which columns should be shown in the report.</p><p>The date and transaction amount are always shown.</p> Show Columns 2 0 0 <p>Select this option to show the Memo column</p> Memo buttonGroup1 <p>Select this option to show the Shares column for investments</p> Shares buttonGroup1 <p>Select this option to show the Price column for investments</p> Price buttonGroup1 <p>Select this option to show the Reconciled column</p> Reconciled buttonGroup1 <p>Select this option to show the Account column</p> Account buttonGroup1 0 0 <p>Select this option to show the Number column</p> Number buttonGroup1 0 0 <p>Select this option to show the Tag column</p> Tag buttonGroup1 0 0 <p>Select this option to show the Payee column</p> Payee buttonGroup1 0 0 <p>Select this option to show the Category column</p> Category buttonGroup1 <p>Select this option to show the Action column</p> Action buttonGroup1 <p>Select this option to show the Running balance column</p> Balance buttonGroup1 Qt::Horizontal QSizePolicy::Expanding 205 20 Include only Loan accounts <p>Check this box to include only those categories which have been marked to "Include on Tax Reports"</p> Include only Investment accounts Do not display the individual transactions that make up a split transaction Hide Split Transaction Details <p>Check this box to include only those categories which have been marked to "Include on Tax Reports"</p> Include only Tax categories Do not display the transactions, leaving only totals displayed. Hide Transactions Hide Totals + + + + Include transfers + + + Qt::Vertical QSizePolicy::Expanding 20 16 KComboBox QComboBox
kcombobox.h
m_comboOrganizeBy m_checkNumber m_checkCategory m_checkTag m_checkAccount m_checkShares m_checkAction m_checkPayee m_checkMemo m_checkReconciled m_checkPrice m_checkBalance m_checkTax m_checkInvestments m_checkLoans m_checkHideSplitDetails m_checkHideTransactions m_checkHideTotals m_checkTax toggled(bool) m_checkInvestments setDisabled(bool) 20 20 20 20 m_checkTax toggled(bool) m_checkLoans setDisabled(bool) 20 20 20 20 m_checkInvestments toggled(bool) m_checkTax setDisabled(bool) 20 20 20 20 m_checkInvestments toggled(bool) m_checkLoans setDisabled(bool) 20 20 20 20 m_checkLoans toggled(bool) m_checkTax setDisabled(bool) 20 20 20 20 m_checkLoans toggled(bool) m_checkInvestments setDisabled(bool) 20 20 20 20
diff --git a/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp b/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp index dabd613ee..753bf5e37 100644 --- a/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp +++ b/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp @@ -1,1258 +1,1260 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2007-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * Copyright 2018 Michael Kiefer * 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, NegExpenses, // 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")}, {Attribute::Report::NegExpenses, QStringLiteral("negexpenses")} }; return attributeNames.value(attributeID); } uint qHash(const Attribute::Report key, uint seed) { Q_UNUSED(seed); return ::qHash(static_cast(key), 0); } 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); } uint qHash(const Attribute::Budget key, uint seed) { Q_UNUSED(seed); return ::qHash(static_cast(key), 0); } 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)), report.isIncludingBudgetActuals()); 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.setNegExpenses(node.attribute(attributeName(Attribute::Report::NegExpenses), "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()); + report.setIncludingTransfers(node.attribute(attributeName(Attribute::Report::IncludesTransfers), "0").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"); + el.setAttribute(attributeName(Attribute::Report::Type), "querytable 1.15"); 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 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::NegExpenses), report.isNegExpenses()); 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())); + el.setAttribute(attributeName(Attribute::Report::IncludesTransfers), report.isIncludingTransfers()); // 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); } }