diff --git a/kmymoney/mymoney/storage/mymoneyseqaccessmgr.cpp b/kmymoney/mymoney/storage/mymoneyseqaccessmgr.cpp index 8b6035a18..f1b469124 100644 --- a/kmymoney/mymoney/storage/mymoneyseqaccessmgr.cpp +++ b/kmymoney/mymoney/storage/mymoneyseqaccessmgr.cpp @@ -1,2121 +1,2121 @@ /*************************************************************************** mymoneyseqaccessmgr.cpp ------------------- begin : Sun May 5 2002 copyright : (C) 2000-2002 by Michael Edwardes 2002 Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyseqaccessmgr.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneystoragesql.h" #include "storageenums.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytag.h" #include "mymoneypayee.h" #include "mymoneybudget.h" #include "mymoneyschedule.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #define TRY try { #define CATCH } catch (const MyMoneyException &e) { #define PASS } catch (const MyMoneyException &e) { throw; } using namespace eStorage; MyMoneySeqAccessMgr::MyMoneySeqAccessMgr() { m_nextAccountID = 0; m_nextInstitutionID = 0; m_nextTransactionID = 0; m_nextPayeeID = 0; m_nextTagID = 0; m_nextScheduleID = 0; m_nextSecurityID = 0; m_nextReportID = 0; m_nextBudgetID = 0; m_nextOnlineJobID = 0; m_nextCostCenterID = 0; m_user = MyMoneyPayee(); m_dirty = false; m_creationDate = QDate::currentDate(); // setup standard accounts MyMoneyAccount acc_l; acc_l.setAccountType(eMyMoney::Account::Type::Liability); acc_l.setName("Liability"); MyMoneyAccount liability(STD_ACC_LIABILITY, acc_l); MyMoneyAccount acc_a; acc_a.setAccountType(eMyMoney::Account::Type::Asset); acc_a.setName("Asset"); MyMoneyAccount asset(STD_ACC_ASSET, acc_a); MyMoneyAccount acc_e; acc_e.setAccountType(eMyMoney::Account::Type::Expense); acc_e.setName("Expense"); MyMoneyAccount expense(STD_ACC_EXPENSE, acc_e); MyMoneyAccount acc_i; acc_i.setAccountType(eMyMoney::Account::Type::Income); acc_i.setName("Income"); MyMoneyAccount income(STD_ACC_INCOME, acc_i); MyMoneyAccount acc_q; acc_q.setAccountType(eMyMoney::Account::Type::Equity); acc_q.setName("Equity"); MyMoneyAccount equity(STD_ACC_EQUITY, acc_q); QMap map; map[STD_ACC_ASSET] = asset; map[STD_ACC_LIABILITY] = liability; map[STD_ACC_INCOME] = income; map[STD_ACC_EXPENSE] = expense; map[STD_ACC_EQUITY] = equity; // load account list with initial accounts m_accountList = map; // initialize for file fixes (see kmymoneyview.cpp) m_currentFixVersion = 4; m_fileFixVersion = 0; // default value if no fix-version in file m_transactionListFull = false; } MyMoneySeqAccessMgr::~MyMoneySeqAccessMgr() { } MyMoneySeqAccessMgr const * MyMoneySeqAccessMgr::duplicate() { MyMoneySeqAccessMgr* that = new MyMoneySeqAccessMgr(); *that = *this; return that; } /** * This method is used to get a SQL reader for subsequent database access */ QExplicitlySharedDataPointer MyMoneySeqAccessMgr::connectToDatabase (const QUrl& /*url*/) { return QExplicitlySharedDataPointer (); } bool MyMoneySeqAccessMgr::isStandardAccount(const QString& id) const { return id == STD_ACC_LIABILITY || id == STD_ACC_ASSET || id == STD_ACC_EXPENSE || id == STD_ACC_INCOME || id == STD_ACC_EQUITY; } void MyMoneySeqAccessMgr::setAccountName(const QString& id, const QString& name) { if (!isStandardAccount(id)) throw MYMONEYEXCEPTION("Only standard accounts can be modified using setAccountName()"); MyMoneyAccount acc = m_accountList[id]; acc.setName(name); m_accountList.modify(acc.id(), acc); } const MyMoneyAccount MyMoneySeqAccessMgr::account(const QString& id) const { // locate the account and if present, return it's data if (m_accountList.find(id) != m_accountList.end()) return m_accountList[id]; // throw an exception, if it does not exist QString msg = "Unknown account id '" + id + '\''; throw MYMONEYEXCEPTION(msg); } void MyMoneySeqAccessMgr::accountList(QList& list) const { QMap::ConstIterator it; for (it = m_accountList.begin(); it != m_accountList.end(); ++it) { if (!isStandardAccount((*it).id())) { list.append(*it); } } } void MyMoneySeqAccessMgr::addAccount(MyMoneyAccount& account) { // create the account. MyMoneyAccount newAccount(nextAccountID(), account); m_accountList.insert(newAccount.id(), newAccount); account = newAccount; } void MyMoneySeqAccessMgr::addPayee(MyMoneyPayee& payee) { // create the payee MyMoneyPayee newPayee(nextPayeeID(), payee); m_payeeList.insert(newPayee.id(), newPayee); payee = newPayee; } /** * @brief Add onlineJob to storage * @param job caller stays owner of the object, but id will be set */ void MyMoneySeqAccessMgr::addOnlineJob(onlineJob &job) { onlineJob newJob = onlineJob(nextOnlineJobID(), job); m_onlineJobList.insert(newJob.id(), newJob); job = newJob; } void MyMoneySeqAccessMgr::removeOnlineJob(const onlineJob& job) { if (!m_onlineJobList.contains(job.id())) { throw MYMONEYEXCEPTION("Unknown onlineJob '" + job.id() + "' should be removed."); } m_onlineJobList.remove(job.id()); } void MyMoneySeqAccessMgr::modifyOnlineJob(const onlineJob &job) { QMap::ConstIterator iter = m_onlineJobList.find(job.id()); if (iter == m_onlineJobList.end()) { throw MYMONEYEXCEPTION("Got unknown onlineJob '" + job.id() + "' for modifying"); } onlineJob oldJob = iter.value(); m_onlineJobList.modify((*iter).id(), job); } const onlineJob MyMoneySeqAccessMgr::getOnlineJob(const QString &id) const { if (m_onlineJobList.contains(id)) { return m_onlineJobList[id]; } - throw MYMONEYEXCEPTION("Unknown online Job '" + id + "'"); + throw MYMONEYEXCEPTION("Unknown online Job '" + id + '\''); } const MyMoneyPayee MyMoneySeqAccessMgr::payee(const QString& id) const { QMap::ConstIterator it; it = m_payeeList.find(id); if (it == m_payeeList.end()) throw MYMONEYEXCEPTION("Unknown payee '" + id + '\''); return *it; } const MyMoneyPayee MyMoneySeqAccessMgr::payeeByName(const QString& payee) const { if (payee.isEmpty()) return MyMoneyPayee::null; QMap::ConstIterator it_p; for (it_p = m_payeeList.begin(); it_p != m_payeeList.end(); ++it_p) { if ((*it_p).name() == payee) { return *it_p; } } throw MYMONEYEXCEPTION("Unknown payee '" + payee + '\''); } void MyMoneySeqAccessMgr::modifyPayee(const MyMoneyPayee& payee) { QMap::ConstIterator it; it = m_payeeList.find(payee.id()); if (it == m_payeeList.end()) { QString msg = "Unknown payee '" + payee.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_payeeList.modify((*it).id(), payee); } void MyMoneySeqAccessMgr::removePayee(const MyMoneyPayee& payee) { QMap::ConstIterator it_t; QMap::ConstIterator it_s; QMap::ConstIterator it_p; it_p = m_payeeList.find(payee.id()); if (it_p == m_payeeList.end()) { QString msg = "Unknown payee '" + payee.id() + '\''; throw MYMONEYEXCEPTION(msg); } // scan all transactions to check if the payee is still referenced for (it_t = m_transactionList.begin(); it_t != m_transactionList.end(); ++it_t) { if ((*it_t).hasReferenceTo(payee.id())) { throw MYMONEYEXCEPTION(QString("Cannot remove payee that is still referenced to a %1").arg("transaction")); } } // check referential integrity in schedules for (it_s = m_scheduleList.begin(); it_s != m_scheduleList.end(); ++it_s) { if ((*it_s).hasReferenceTo(payee.id())) { throw MYMONEYEXCEPTION(QString("Cannot remove payee that is still referenced to a %1").arg("schedule")); } } // remove any reference to report and/or budget removeReferences(payee.id()); m_payeeList.remove((*it_p).id()); } const QList MyMoneySeqAccessMgr::payeeList() const { return m_payeeList.values(); } void MyMoneySeqAccessMgr::addTag(MyMoneyTag& tag) { // create the tag MyMoneyTag newTag(nextTagID(), tag); m_tagList.insert(newTag.id(), newTag); tag = newTag; } const MyMoneyTag MyMoneySeqAccessMgr::tag(const QString& id) const { QMap::ConstIterator it; it = m_tagList.find(id); if (it == m_tagList.end()) throw MYMONEYEXCEPTION("Unknown tag '" + id + '\''); return *it; } const MyMoneyTag MyMoneySeqAccessMgr::tagByName(const QString& tag) const { if (tag.isEmpty()) return MyMoneyTag::null; QMap::ConstIterator it_ta; for (it_ta = m_tagList.begin(); it_ta != m_tagList.end(); ++it_ta) { if ((*it_ta).name() == tag) { return *it_ta; } } throw MYMONEYEXCEPTION("Unknown tag '" + tag + '\''); } void MyMoneySeqAccessMgr::modifyTag(const MyMoneyTag& tag) { QMap::ConstIterator it; it = m_tagList.find(tag.id()); if (it == m_tagList.end()) { QString msg = "Unknown tag '" + tag.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_tagList.modify((*it).id(), tag); } void MyMoneySeqAccessMgr::removeTag(const MyMoneyTag& tag) { QMap::ConstIterator it_t; QMap::ConstIterator it_s; QMap::ConstIterator it_ta; it_ta = m_tagList.find(tag.id()); if (it_ta == m_tagList.end()) { QString msg = "Unknown tag '" + tag.id() + '\''; throw MYMONEYEXCEPTION(msg); } // scan all transactions to check if the tag is still referenced for (it_t = m_transactionList.begin(); it_t != m_transactionList.end(); ++it_t) { if ((*it_t).hasReferenceTo(tag.id())) { throw MYMONEYEXCEPTION(QString("Cannot remove tag that is still referenced to a %1").arg("transaction")); } } // check referential integrity in schedules for (it_s = m_scheduleList.begin(); it_s != m_scheduleList.end(); ++it_s) { if ((*it_s).hasReferenceTo(tag.id())) { throw MYMONEYEXCEPTION(QString("Cannot remove tag that is still referenced to a %1").arg("schedule")); } } // remove any reference to report and/or budget removeReferences(tag.id()); m_tagList.remove((*it_ta).id()); } const QList MyMoneySeqAccessMgr::tagList() const { return m_tagList.values(); } void MyMoneySeqAccessMgr::addAccount(MyMoneyAccount& parent, MyMoneyAccount& account) { QMap::ConstIterator theParent; QMap::ConstIterator theChild; theParent = m_accountList.find(parent.id()); if (theParent == m_accountList.end()) { QString msg = "Unknown parent account '"; msg += parent.id() + '\''; throw MYMONEYEXCEPTION(msg); } theChild = m_accountList.find(account.id()); if (theChild == m_accountList.end()) { QString msg = "Unknown child account '"; msg += account.id() + '\''; throw MYMONEYEXCEPTION(msg); } MyMoneyAccount acc = *theParent; acc.addAccountId(account.id()); m_accountList.modify(acc.id(), acc); parent = acc; acc = *theChild; acc.setParentAccountId(parent.id()); m_accountList.modify(acc.id(), acc); account = acc; } void MyMoneySeqAccessMgr::addInstitution(MyMoneyInstitution& institution) { MyMoneyInstitution newInstitution(nextInstitutionID(), institution); m_institutionList.insert(newInstitution.id(), newInstitution); // return new data institution = newInstitution; } unsigned int MyMoneySeqAccessMgr::transactionCount(const QString& account) const { unsigned int cnt = 0; if (account.length() == 0) { cnt = m_transactionList.count(); } else { QMap::ConstIterator it_t; QList::ConstIterator it_s; // scan all transactions for (it_t = m_transactionList.begin(); it_t != m_transactionList.end(); ++it_t) { // scan all splits of this transaction for (it_s = (*it_t).splits().begin(); it_s != (*it_t).splits().end(); ++it_s) { // is it a split in our account? if ((*it_s).accountId() == account) { // since a transaction can only have one split referencing // each account, we're done with the splits here! break; } } // if no split contains the account id, continue with the // next transaction if (it_s == (*it_t).splits().end()) continue; // otherwise count it ++cnt; } } return cnt; } const QMap MyMoneySeqAccessMgr::transactionCountMap() const { QMap map; QMap::ConstIterator it_t; QList::ConstIterator it_s; // scan all transactions for (it_t = m_transactionList.begin(); it_t != m_transactionList.end(); ++it_t) { // scan all splits of this transaction for (it_s = (*it_t).splits().begin(); it_s != (*it_t).splits().end(); ++it_s) { map[(*it_s).accountId()]++; } } return map; } unsigned int MyMoneySeqAccessMgr::institutionCount() const { return m_institutionList.count(); } unsigned int MyMoneySeqAccessMgr::accountCount() const { return m_accountList.count(); } QString MyMoneySeqAccessMgr::nextPayeeID() { QString id; id.setNum(++m_nextPayeeID); id = 'P' + id.rightJustified(PAYEE_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextTagID() { QString id; id.setNum(++m_nextTagID); id = 'G' + id.rightJustified(TAG_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextInstitutionID() { QString id; id.setNum(++m_nextInstitutionID); id = 'I' + id.rightJustified(INSTITUTION_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextAccountID() { QString id; id.setNum(++m_nextAccountID); id = 'A' + id.rightJustified(ACCOUNT_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextTransactionID() { QString id; id.setNum(++m_nextTransactionID); id = 'T' + id.rightJustified(TRANSACTION_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextScheduleID() { QString id; id.setNum(++m_nextScheduleID); id = "SCH" + id.rightJustified(SCHEDULE_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextSecurityID() { QString id; id.setNum(++m_nextSecurityID); id = 'E' + id.rightJustified(SECURITY_ID_SIZE, '0'); return id; } QString MyMoneySeqAccessMgr::nextOnlineJobID() { QString id; id.setNum(++m_nextOnlineJobID); id = 'O' + id.rightJustified(ONLINE_JOB_ID_SIZE, '0'); return id; } void MyMoneySeqAccessMgr::addTransaction(MyMoneyTransaction& transaction, const bool skipAccountUpdate) { // 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("transaction already contains an id"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION("invalid post date"); // now check the splits QList::ConstIterator it_s; for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { // the following lines will throw an exception if the // account or payee do not exist account((*it_s).accountId()); if (!(*it_s).payeeId().isEmpty()) payee((*it_s).payeeId()); } MyMoneyTransaction newTransaction(nextTransactionID(), transaction); QString key = newTransaction.uniqueSortKey(); m_transactionList.insert(key, newTransaction); m_transactionKeys.insert(newTransaction.id(), key); transaction = newTransaction; // adjust the balance of all affected accounts for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { MyMoneyAccount acc = m_accountList[(*it_s).accountId()]; adjustBalance(acc, *it_s); if (!skipAccountUpdate) { acc.touch(); } m_accountList.modify(acc.id(), acc); } } void MyMoneySeqAccessMgr::adjustBalance(MyMoneyAccount& acc, const MyMoneySplit& split, bool reverse) { // in case of an investment we can't just add or subtract the // amount of the split since we don't know about stock splits. // so in the case of those stocks, we simply recalculate the balance from scratch if (acc.isInvest()) { acc.setBalance(calculateBalance(acc.id())); } else { acc.adjustBalance(split, reverse); } } void MyMoneySeqAccessMgr::touch() { m_dirty = true; m_lastModificationDate = QDate::currentDate(); } bool MyMoneySeqAccessMgr::hasActiveSplits(const QString& id) const { QMap::ConstIterator it; for (it = m_transactionList.begin(); it != m_transactionList.end(); ++it) { if ((*it).accountReferenced(id)) { return true; } } return false; } const MyMoneyInstitution MyMoneySeqAccessMgr::institution(const QString& id) const { QMap::ConstIterator pos; pos = m_institutionList.find(id); if (pos != m_institutionList.end()) return *pos; throw MYMONEYEXCEPTION("unknown institution"); } const QList MyMoneySeqAccessMgr::institutionList() const { return m_institutionList.values(); } void MyMoneySeqAccessMgr::modifyAccount(const MyMoneyAccount& account, const bool skipCheck) { QMap::ConstIterator pos; // locate the account in the file global pool pos = m_accountList.find(account.id()); if (pos != m_accountList.end()) { // check if the new info is based on the old one. // this is the case, when the file and the id // as well as the type are equal. if (((*pos).parentAccountId() == account.parentAccountId() && ((*pos).accountType() == account.accountType() || ((*pos).isLiquidAsset() && account.isLiquidAsset()))) || skipCheck == true) { // make sure that all the referenced objects exist if (!account.institutionId().isEmpty()) institution(account.institutionId()); QList::ConstIterator it_a; for (it_a = account.accountList().constBegin(); it_a != account.accountList().constEnd(); ++it_a) { this->account(*it_a); } // update information in account list m_accountList.modify(account.id(), account); } else throw MYMONEYEXCEPTION("Invalid information for update"); } else throw MYMONEYEXCEPTION("Unknown account id"); } void MyMoneySeqAccessMgr::modifyInstitution(const MyMoneyInstitution& institution) { QMap::ConstIterator pos; // locate the institution in the file global pool pos = m_institutionList.find(institution.id()); if (pos != m_institutionList.end()) { m_institutionList.modify(institution.id(), institution); } else throw MYMONEYEXCEPTION("unknown institution"); } void MyMoneySeqAccessMgr::modifyTransaction(const MyMoneyTransaction& transaction) { // perform some checks to see that the transaction stuff is OK. For // now we assume that // * ids are assigned // * the pointer to the MyMoneyFile object is not 0 // * the date valid (must not be empty) // * the splits must have valid account ids // first perform all the checks if (transaction.id().isEmpty() // || transaction.file() != this || !transaction.postDate().isValid()) throw MYMONEYEXCEPTION("invalid transaction to be modified"); // now check the splits QList::ConstIterator it_s; for (it_s = transaction.splits().begin(); it_s != transaction.splits().end(); ++it_s) { // the following lines will throw an exception if the // account or payee do not exist account((*it_s).accountId()); if (!(*it_s).payeeId().isEmpty()) payee((*it_s).payeeId()); foreach (const QString& tagId, (*it_s).tagIdList()) { if (!tagId.isEmpty()) tag(tagId); } } // new data seems to be ok. find old version of transaction // in our pool. Throw exception if unknown. if (!m_transactionKeys.contains(transaction.id())) throw MYMONEYEXCEPTION("invalid transaction id"); QString oldKey = m_transactionKeys[transaction.id()]; if (!m_transactionList.contains(oldKey)) throw MYMONEYEXCEPTION("invalid transaction key"); QMap::ConstIterator it_t; it_t = m_transactionList.find(oldKey); if (it_t == m_transactionList.end()) throw MYMONEYEXCEPTION("invalid transaction key"); for (it_s = (*it_t).splits().begin(); it_s != (*it_t).splits().end(); ++it_s) { MyMoneyAccount acc = m_accountList[(*it_s).accountId()]; // we only need to adjust non-investment accounts here // as for investment accounts the balance will be recalculated // after the transaction has been added. if (!acc.isInvest()) { adjustBalance(acc, *it_s, true); acc.touch(); m_accountList.modify(acc.id(), acc); } } // remove old transaction from lists m_transactionList.remove(oldKey); // add new transaction to lists QString newKey = transaction.uniqueSortKey(); m_transactionList.insert(newKey, transaction); m_transactionKeys.modify(transaction.id(), newKey); // adjust account balances for (it_s = transaction.splits().begin(); it_s != transaction.splits().end(); ++it_s) { MyMoneyAccount acc = m_accountList[(*it_s).accountId()]; adjustBalance(acc, *it_s); acc.touch(); m_accountList.modify(acc.id(), acc); } } void MyMoneySeqAccessMgr::reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent) { reparentAccount(account, parent, true); } void MyMoneySeqAccessMgr::reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent, const bool /* sendNotification */) { QMap::ConstIterator oldParent; QMap::ConstIterator newParent; QMap::ConstIterator childAccount; // verify that accounts exist. If one does not, // an exception is thrown MyMoneySeqAccessMgr::account(account.id()); MyMoneySeqAccessMgr::account(parent.id()); if (!account.parentAccountId().isEmpty()) { MyMoneySeqAccessMgr::account(account.parentAccountId()); oldParent = m_accountList.find(account.parentAccountId()); } if (account.accountType() == eMyMoney::Account::Type::Stock && parent.accountType() != eMyMoney::Account::Type::Investment) throw MYMONEYEXCEPTION("Cannot move a stock acocunt into a non-investment account"); newParent = m_accountList.find(parent.id()); childAccount = m_accountList.find(account.id()); MyMoneyAccount acc; if (!account.parentAccountId().isEmpty()) { acc = (*oldParent); acc.removeAccountId(account.id()); m_accountList.modify(acc.id(), acc); } parent = (*newParent); parent.addAccountId(account.id()); m_accountList.modify(parent.id(), parent); account = (*childAccount); account.setParentAccountId(parent.id()); m_accountList.modify(account.id(), account); #if 0 // make sure the type is the same as the new parent. This does not work for stock and investment if (account.accountType() != eMyMoney::Account::Type::Stock && account.accountType() != eMyMoney::Account::Type::Investment) (*childAccount).setAccountType((*newParent).accountType()); #endif } void MyMoneySeqAccessMgr::removeTransaction(const MyMoneyTransaction& transaction) { // first perform all the checks if (transaction.id().isEmpty()) throw MYMONEYEXCEPTION("invalid transaction to be deleted"); QMap::ConstIterator it_k; QMap::ConstIterator it_t; it_k = m_transactionKeys.find(transaction.id()); if (it_k == m_transactionKeys.end()) throw MYMONEYEXCEPTION("invalid transaction to be deleted"); it_t = m_transactionList.find(*it_k); if (it_t == m_transactionList.end()) throw MYMONEYEXCEPTION("invalid transaction key"); // keep a copy so that we still have the data after removal MyMoneyTransaction t(*it_t); // FIXME: check if any split is frozen and throw exception // remove the transaction from the two lists m_transactionList.remove(*it_k); m_transactionKeys.remove(transaction.id()); // scan the splits and collect all accounts that need // to be updated after the removal of this transaction QList::ConstIterator it_s; for (it_s = t.splits().constBegin(); it_s != t.splits().constEnd(); ++it_s) { MyMoneyAccount acc = m_accountList[(*it_s).accountId()]; adjustBalance(acc, *it_s, true); acc.touch(); m_accountList.modify(acc.id(), acc); } } void MyMoneySeqAccessMgr::removeAccount(const MyMoneyAccount& account) { MyMoneyAccount parent; // check that the account and it's parent exist // this will throw an exception if the id is unknown MyMoneySeqAccessMgr::account(account.id()); parent = MyMoneySeqAccessMgr::account(account.parentAccountId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION("Unable to remove account with active splits"); } // re-parent all sub-ordinate accounts to the parent of the account // to be deleted. First round check that all accounts exist, second // round do the re-parenting. foreach (const auto accountID, account.accountList()) MyMoneySeqAccessMgr::account(accountID); // if one of the accounts did not exist, an exception had been // thrown and we would not make it until here. QMap::ConstIterator it_a; QMap::ConstIterator it_p; // locate the account in the file global pool it_a = m_accountList.find(account.id()); if (it_a == m_accountList.end()) throw MYMONEYEXCEPTION("Internal error: account not found in list"); it_p = m_accountList.find(parent.id()); if (it_p == m_accountList.end()) throw MYMONEYEXCEPTION("Internal error: parent account not found in list"); if (!account.institutionId().isEmpty()) throw MYMONEYEXCEPTION("Cannot remove account still attached to an institution"); removeReferences(account.id()); // FIXME: check referential integrity for the account to be removed // check if the new info is based on the old one. // this is the case, when the file and the id // as well as the type are equal. if ((*it_a).id() == account.id() && (*it_a).accountType() == account.accountType()) { // second round over sub-ordinate accounts: do re-parenting // but only if the list contains at least one entry // FIXME: move this logic to MyMoneyFile foreach (const auto accountID, (*it_a).accountList()) { MyMoneyAccount acc(MyMoneySeqAccessMgr::account(accountID)); reparentAccount(acc, parent, false); } // remove account from parent's list parent.removeAccountId(account.id()); m_accountList.modify(parent.id(), parent); // remove account from the global account pool m_accountList.remove(account.id()); } } void MyMoneySeqAccessMgr::removeInstitution(const MyMoneyInstitution& institution) { QMap::ConstIterator it_i; it_i = m_institutionList.find(institution.id()); if (it_i != m_institutionList.end()) { m_institutionList.remove(institution.id()); } else throw MYMONEYEXCEPTION("invalid institution"); } void MyMoneySeqAccessMgr::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { list.clear(); QMap::ConstIterator it_t; QMap::ConstIterator it_t_end = m_transactionList.end(); for (it_t = m_transactionList.begin(); it_t != it_t_end; ++it_t) { // This code is used now. It adds the transaction to the list for // each matching split exactly once. This allows to show information // about different splits in the same register view (e.g. search result) // // I have no idea, if this has some impact on the functionality. So far, // I could not see it. (ipwizard 9/5/2003) if (filter.match(*it_t)) { unsigned int cnt = filter.matchingSplits().count(); if (cnt > 1) { for (unsigned i = 0; i < cnt; ++i) list.append(*it_t); } else { list.append(*it_t); } } } } void MyMoneySeqAccessMgr::transactionList(QList< QPair >& list, MyMoneyTransactionFilter& filter) const { list.clear(); QMap::ConstIterator it_t; QMap::ConstIterator it_t_end = m_transactionList.end(); for (it_t = m_transactionList.begin(); it_t != it_t_end; ++it_t) { if (filter.match(*it_t)) { foreach (const auto split, filter.matchingSplits()) list.append(qMakePair(*it_t, split)); } } } const QList MyMoneySeqAccessMgr::transactionList(MyMoneyTransactionFilter& filter) const { QList list; transactionList(list, filter); return list; } const QList MyMoneySeqAccessMgr::onlineJobList() const { return m_onlineJobList.values(); } const QList< MyMoneyCostCenter > MyMoneySeqAccessMgr::costCenterList() const { return m_costCenterList.values(); } const MyMoneyCostCenter MyMoneySeqAccessMgr::costCenter(const QString& id) const { if (!m_costCenterList.contains(id)) { QString msg = QString("Invalid cost center id '%1'").arg(id); throw MYMONEYEXCEPTION(msg); } return m_costCenterList[id]; } const MyMoneyTransaction MyMoneySeqAccessMgr::transaction(const QString& id) const { // get the full key of this transaction, throw exception // if it's invalid (unknown) if (!m_transactionKeys.contains(id)) { QString msg = QString("Invalid transaction id '%1'").arg(id); throw MYMONEYEXCEPTION(msg); } // check if this key is in the list, throw exception if not QString key = m_transactionKeys[id]; if (!m_transactionList.contains(key)) { QString msg = QString("Invalid transaction key '%1'").arg(key); throw MYMONEYEXCEPTION(msg); } return m_transactionList[key]; } const MyMoneyTransaction MyMoneySeqAccessMgr::transaction(const QString& account, const int idx) const { /* removed with MyMoneyAccount::Transaction QMap::ConstIterator acc; // find account object in list, throw exception if unknown acc = m_accountList.find(account); if(acc == m_accountList.end()) throw MYMONEYEXCEPTION("unknown account id"); // get the transaction info from the account MyMoneyAccount::Transaction t = (*acc).transaction(idx); // return the transaction, throw exception if not found return transaction(t.transactionID()); */ // new implementation if the above code does not work anymore QList list; MyMoneyAccount acc = m_accountList[account]; MyMoneyTransactionFilter filter; if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) filter.addCategory(account); else filter.addAccount(account); transactionList(list, filter); if (idx < 0 || idx >= static_cast(list.count())) throw MYMONEYEXCEPTION("Unknown idx for transaction"); return transaction(list[idx].id()); } const MyMoneyMoney MyMoneySeqAccessMgr::balance(const QString& id, const QDate& date) const { MyMoneyAccount acc = account(id); if (!date.isValid()) { // the balance of all transactions for this account has // been requested. no need to calculate anything as we // have this number with the account object already. if (m_accountList.find(id) != m_accountList.end()) { return m_accountList[id].balance(); } return MyMoneyMoney(); } return calculateBalance(id, date); } MyMoneyMoney MyMoneySeqAccessMgr::calculateBalance(const QString& id, const QDate& date) const { MyMoneyMoney balance; QList list; MyMoneyTransactionFilter filter; filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); 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; if (split.action() == MyMoneySplit::ActionSplitShares) { balance = balance * split.shares(); } else { balance += split.shares(); } } } return balance; } const MyMoneyMoney MyMoneySeqAccessMgr::totalBalance(const QString& id, const QDate& date) const { QStringList accounts; QStringList::ConstIterator it_a; MyMoneyMoney result(balance(id, date)); accounts = account(id).accountList(); for (it_a = accounts.constBegin(); it_a != accounts.constEnd(); ++it_a) { result += totalBalance(*it_a, date); } return result; } MyMoneyAccount MyMoneySeqAccessMgr::liability() const { return account(STD_ACC_LIABILITY); } MyMoneyAccount MyMoneySeqAccessMgr::asset() const { return account(STD_ACC_ASSET); } MyMoneyAccount MyMoneySeqAccessMgr::expense() const { return account(STD_ACC_EXPENSE); } MyMoneyAccount MyMoneySeqAccessMgr::income() const { return account(STD_ACC_INCOME); } MyMoneyAccount MyMoneySeqAccessMgr::equity() const { return account(STD_ACC_EQUITY); } void MyMoneySeqAccessMgr::loadAccounts(const QMap& map) { m_accountList = map; // scan the map to identify the last used id QMap::const_iterator it_a; QString lastId(""); for (it_a = map.begin(); it_a != map.end(); ++it_a) { if (!isStandardAccount((*it_a).id()) && ((*it_a).id() > lastId)) lastId = (*it_a).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextAccountID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadTransactions(const QMap& map) { m_transactionList = map; // now fill the key map and // identify the last used id QString lastId(""); QMap keys; QMap::ConstIterator it_t; for (it_t = map.constBegin(); it_t != map.constEnd(); ++it_t) { keys[(*it_t).id()] = it_t.key(); if ((*it_t).id() > lastId) lastId = (*it_t).id(); } m_transactionKeys = keys; // determine highest used ID so far int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextTransactionID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadInstitutions(const QMap& map) { m_institutionList = map; // scan the map to identify the last used id QMap::const_iterator it_i; QString lastId(""); for (it_i = map.begin(); it_i != map.end(); ++it_i) { if ((*it_i).id() > lastId) lastId = (*it_i).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextInstitutionID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadPayees(const QMap& map) { m_payeeList = map; // scan the map to identify the last used id QMap::const_iterator it_p; QString lastId(""); for (it_p = map.begin(); it_p != map.end(); ++it_p) { if ((*it_p).id().length() <= PAYEE_ID_SIZE + 1) { if ((*it_p).id() > lastId) lastId = (*it_p).id(); } else { } } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextPayeeID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadTags(const QMap& map) { m_tagList = map; // scan the map to identify the last used id QMap::const_iterator it_ta; QString lastId(""); for (it_ta = map.begin(); it_ta != map.end(); ++it_ta) { if ((*it_ta).id().length() <= TAG_ID_SIZE + 1) { if ((*it_ta).id() > lastId) lastId = (*it_ta).id(); } else { } } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextTagID = lastId.mid(pos).toUInt(); } } void MyMoneySeqAccessMgr::loadSecurities(const QMap& map) { m_securitiesList = map; // scan the map to identify the last used id QMap::const_iterator it_s; QString lastId(""); for (it_s = map.begin(); it_s != map.end(); ++it_s) { if ((*it_s).id() > lastId) lastId = (*it_s).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextSecurityID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadCurrencies(const QMap& map) { m_currencyList = map; } void MyMoneySeqAccessMgr::loadPrices(const MyMoneyPriceList& list) { m_priceList = list; } void MyMoneySeqAccessMgr::loadOnlineJobs(const QMap< QString, onlineJob >& onlineJobs) { m_onlineJobList = onlineJobs; QString lastId(""); const QMap< QString, onlineJob >::const_iterator end = onlineJobs.constEnd(); for (QMap< QString, onlineJob >::const_iterator iter = onlineJobs.constBegin(); iter != end; ++iter) { if ((*iter).id() > lastId) lastId = (*iter).id(); } const int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextOnlineJobID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadCostCenters(const QMap< QString, MyMoneyCostCenter >& costCenters) { m_costCenterList = costCenters; // scan the map to identify the last used id QMap::const_iterator it_s; QString lastId; for (it_s = costCenters.constBegin(); it_s != costCenters.constEnd(); ++it_s) { if ((*it_s).id() > lastId) lastId = (*it_s).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextCostCenterID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadAccountId(const unsigned long id) { m_nextAccountID = id; } void MyMoneySeqAccessMgr::loadTransactionId(const unsigned long id) { m_nextTransactionID = id; } void MyMoneySeqAccessMgr::loadPayeeId(const unsigned long id) { m_nextPayeeID = id; } void MyMoneySeqAccessMgr::loadTagId(const unsigned long id) { m_nextTagID = id; } void MyMoneySeqAccessMgr::loadInstitutionId(const unsigned long id) { m_nextInstitutionID = id; } void MyMoneySeqAccessMgr::loadSecurityId(const unsigned long id) { m_nextSecurityID = id; } void MyMoneySeqAccessMgr::loadReportId(const unsigned long id) { m_nextReportID = id; } void MyMoneySeqAccessMgr::loadBudgetId(const unsigned long id) { m_nextBudgetID = id; } void MyMoneySeqAccessMgr::loadOnlineJobId(const long unsigned int id) { m_nextOnlineJobID = id; } void MyMoneySeqAccessMgr::loadCostCenterId(const long unsigned int id) { m_nextCostCenterID = id; } const QString MyMoneySeqAccessMgr::value(const QString& key) const { return MyMoneyKeyValueContainer::value(key); } void MyMoneySeqAccessMgr::setValue(const QString& key, const QString& val) { MyMoneyKeyValueContainer::setValue(key, val); touch(); } void MyMoneySeqAccessMgr::deletePair(const QString& key) { MyMoneyKeyValueContainer::deletePair(key); touch(); } const QMap MyMoneySeqAccessMgr::pairs() const { return MyMoneyKeyValueContainer::pairs(); } void MyMoneySeqAccessMgr::setPairs(const QMap& list) { MyMoneyKeyValueContainer::setPairs(list); touch(); } void MyMoneySeqAccessMgr::addSchedule(MyMoneySchedule& sched) { // first perform all the checks if (!sched.id().isEmpty()) throw MYMONEYEXCEPTION("schedule already contains an id"); // The following will throw an exception when it fails sched.validate(false); MyMoneySchedule newSched(nextScheduleID(), sched); m_scheduleList.insert(newSched.id(), newSched); sched = newSched; } void MyMoneySeqAccessMgr::modifySchedule(const MyMoneySchedule& sched) { QMap::ConstIterator it; it = m_scheduleList.find(sched.id()); if (it == m_scheduleList.end()) { QString msg = "Unknown schedule '" + sched.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_scheduleList.modify(sched.id(), sched); } void MyMoneySeqAccessMgr::removeSchedule(const MyMoneySchedule& sched) { QMap::ConstIterator it; it = m_scheduleList.find(sched.id()); if (it == m_scheduleList.end()) { QString msg = "Unknown schedule '" + sched.id() + '\''; throw MYMONEYEXCEPTION(msg); } // FIXME: check referential integrity for loan accounts m_scheduleList.remove(sched.id()); } const MyMoneySchedule MyMoneySeqAccessMgr::schedule(const QString& id) const { QMap::ConstIterator pos; // locate the schedule and if present, return it's data pos = m_scheduleList.find(id); if (pos != m_scheduleList.end()) return (*pos); // throw an exception, if it does not exist QString msg = "Unknown schedule id '" + id + '\''; throw MYMONEYEXCEPTION(msg); } const QList MyMoneySeqAccessMgr::scheduleList( const QString& accountId, const eMyMoney::Schedule::Type type, const eMyMoney::Schedule::Occurrence occurrence, const eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { QMap::ConstIterator pos; QList list; // qDebug("scheduleList()"); for (pos = m_scheduleList.begin(); pos != m_scheduleList.end(); ++pos) { // qDebug(" '%s'", qPrintable((*pos).id())); if (type != eMyMoney::Schedule::Type::Any) { if (type != (*pos).type()) { continue; } } if (occurrence != eMyMoney::Schedule::Occurrence::Any) { if (occurrence != (*pos).occurrence()) { continue; } } if (paymentType != eMyMoney::Schedule::PaymentType::Any) { if (paymentType != (*pos).paymentType()) { continue; } } if (!accountId.isEmpty()) { MyMoneyTransaction t = (*pos).transaction(); QList::ConstIterator it; QList splits; splits = t.splits(); for (it = splits.constBegin(); it != splits.constEnd(); ++it) { if ((*it).accountId() == accountId) break; } if (it == splits.constEnd()) { continue; } } if (startDate.isValid() && endDate.isValid()) { if ((*pos).paymentDates(startDate, endDate).count() == 0) { continue; } } if (startDate.isValid() && !endDate.isValid()) { if (!(*pos).nextPayment(startDate.addDays(-1)).isValid()) { continue; } } if (!startDate.isValid() && endDate.isValid()) { if ((*pos).startDate() > endDate) { continue; } } if (overdue) { if (!(*pos).isOverdue()) continue; } // qDebug("Adding '%s'", (*pos).name().toLatin1()); list << *pos; } return list; } void MyMoneySeqAccessMgr::loadSchedules(const QMap& map) { m_scheduleList = map; // scan the map to identify the last used id QMap::const_iterator it_s; QString lastId(""); for (it_s = map.begin(); it_s != map.end(); ++it_s) { if ((*it_s).id() > lastId) lastId = (*it_s).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextScheduleID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::loadScheduleId(const unsigned long id) { m_nextScheduleID = id; } const QList MyMoneySeqAccessMgr::scheduleListEx(int scheduleTypes, int scheduleOcurrences, int schedulePaymentTypes, QDate date, const QStringList& accounts) const { // qDebug("scheduleListEx"); QMap::ConstIterator pos; QList list; if (!date.isValid()) return list; for (pos = m_scheduleList.begin(); pos != m_scheduleList.end(); ++pos) { if (scheduleTypes && !(scheduleTypes & (int)(*pos).type())) continue; if (scheduleOcurrences && !(scheduleOcurrences & (int)(*pos).occurrence())) continue; if (schedulePaymentTypes && !(schedulePaymentTypes & (int)(*pos).paymentType())) continue; if ((*pos).paymentDates(date, date).count() == 0) continue; if ((*pos).isFinished()) continue; if ((*pos).hasRecordedPayment(date)) continue; if (accounts.count() > 0) { if (accounts.contains((*pos).account().id())) continue; } // qDebug("\tAdding '%s'", (*pos).name().toLatin1()); list << *pos; } return list; } void MyMoneySeqAccessMgr::addSecurity(MyMoneySecurity& security) { // create the account MyMoneySecurity newSecurity(nextSecurityID(), security); m_securitiesList.insert(newSecurity.id(), newSecurity); security = newSecurity; } void MyMoneySeqAccessMgr::modifySecurity(const MyMoneySecurity& security) { QMap::ConstIterator it; it = m_securitiesList.find(security.id()); if (it == m_securitiesList.end()) { QString msg = "Unknown security '"; msg += security.id() + "' during modifySecurity()"; throw MYMONEYEXCEPTION(msg); } m_securitiesList.modify(security.id(), security); } void MyMoneySeqAccessMgr::removeSecurity(const MyMoneySecurity& security) { QMap::ConstIterator it; // FIXME: check referential integrity it = m_securitiesList.find(security.id()); if (it == m_securitiesList.end()) { QString msg = "Unknown security '"; msg += security.id() + "' during removeSecurity()"; throw MYMONEYEXCEPTION(msg); } m_securitiesList.remove(security.id()); } const MyMoneySecurity MyMoneySeqAccessMgr::security(const QString& id) const { QMap::ConstIterator it = m_securitiesList.find(id); if (it != m_securitiesList.end()) { return it.value(); } return MyMoneySecurity(); } const QList MyMoneySeqAccessMgr::securityList() const { //qDebug("securityList: Security list size is %d, this=%8p", m_equitiesList.size(), (void*)this); return m_securitiesList.values(); } void MyMoneySeqAccessMgr::addCurrency(const MyMoneySecurity& currency) { QMap::ConstIterator it; it = m_currencyList.find(currency.id()); if (it != m_currencyList.end()) { throw MYMONEYEXCEPTION(i18n("Cannot add currency with existing id %1", currency.id())); } m_currencyList.insert(currency.id(), currency); } void MyMoneySeqAccessMgr::modifyCurrency(const MyMoneySecurity& currency) { QMap::ConstIterator it; it = m_currencyList.find(currency.id()); if (it == m_currencyList.end()) { throw MYMONEYEXCEPTION(i18n("Cannot modify currency with unknown id %1", currency.id())); } m_currencyList.modify(currency.id(), currency); } void MyMoneySeqAccessMgr::removeCurrency(const MyMoneySecurity& currency) { QMap::ConstIterator it; // FIXME: check referential integrity it = m_currencyList.find(currency.id()); if (it == m_currencyList.end()) { throw MYMONEYEXCEPTION(i18n("Cannot remove currency with unknown id %1", currency.id())); } m_currencyList.remove(currency.id()); } const MyMoneySecurity MyMoneySeqAccessMgr::currency(const QString& id) const { if (id.isEmpty()) { } QMap::ConstIterator it; it = m_currencyList.find(id); if (it == m_currencyList.end()) { throw MYMONEYEXCEPTION(i18n("Cannot retrieve currency with unknown id '%1'", id)); } return *it; } const QList MyMoneySeqAccessMgr::currencyList() const { return m_currencyList.values(); } const QList MyMoneySeqAccessMgr::reportList() const { return m_reportList.values(); } void MyMoneySeqAccessMgr::addReport(MyMoneyReport& report) { if (!report.id().isEmpty()) throw MYMONEYEXCEPTION("report already contains an id"); MyMoneyReport newReport(nextReportID(), report); m_reportList.insert(newReport.id(), newReport); report = newReport; } void MyMoneySeqAccessMgr::loadReports(const QMap& map) { m_reportList = map; // scan the map to identify the last used id QMap::const_iterator it_r; QString lastId(""); for (it_r = map.begin(); it_r != map.end(); ++it_r) { if ((*it_r).id() > lastId) lastId = (*it_r).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextReportID = lastId.mid(pos).toInt(); } } void MyMoneySeqAccessMgr::modifyReport(const MyMoneyReport& report) { QMap::ConstIterator it; it = m_reportList.find(report.id()); if (it == m_reportList.end()) { QString msg = "Unknown report '" + report.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_reportList.modify(report.id(), report); } QString MyMoneySeqAccessMgr::nextReportID() { QString id; id.setNum(++m_nextReportID); id = 'R' + id.rightJustified(REPORT_ID_SIZE, '0'); return id; } unsigned MyMoneySeqAccessMgr::countReports() const { return m_reportList.count(); } const MyMoneyReport MyMoneySeqAccessMgr::report(const QString& _id) const { return m_reportList[_id]; } void MyMoneySeqAccessMgr::removeReport(const MyMoneyReport& report) { QMap::ConstIterator it; it = m_reportList.find(report.id()); if (it == m_reportList.end()) { QString msg = "Unknown report '" + report.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_reportList.remove(report.id()); } const QList MyMoneySeqAccessMgr::budgetList() const { return m_budgetList.values(); } void MyMoneySeqAccessMgr::addBudget(MyMoneyBudget& budget) { MyMoneyBudget newBudget(nextBudgetID(), budget); m_budgetList.insert(newBudget.id(), newBudget); budget = newBudget; } void MyMoneySeqAccessMgr::loadBudgets(const QMap& map) { m_budgetList = map; // scan the map to identify the last used id QMap::const_iterator it_b; QString lastId(""); for (it_b = map.begin(); it_b != map.end(); ++it_b) { if ((*it_b).id() > lastId) lastId = (*it_b).id(); } int pos = lastId.indexOf(QRegExp("\\d+"), 0); if (pos != -1) { m_nextBudgetID = lastId.mid(pos).toInt(); } } const MyMoneyBudget MyMoneySeqAccessMgr::budgetByName(const QString& budget) const { QMap::ConstIterator it_p; for (it_p = m_budgetList.begin(); it_p != m_budgetList.end(); ++it_p) { if ((*it_p).name() == budget) { return *it_p; } } throw MYMONEYEXCEPTION("Unknown budget '" + budget + '\''); } void MyMoneySeqAccessMgr::modifyBudget(const MyMoneyBudget& budget) { QMap::ConstIterator it; it = m_budgetList.find(budget.id()); if (it == m_budgetList.end()) { QString msg = "Unknown budget '" + budget.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_budgetList.modify(budget.id(), budget); } QString MyMoneySeqAccessMgr::nextBudgetID() { QString id; id.setNum(++m_nextBudgetID); id = 'B' + id.rightJustified(BUDGET_ID_SIZE, '0'); return id; } unsigned MyMoneySeqAccessMgr::countBudgets() const { return m_budgetList.count(); } MyMoneyBudget MyMoneySeqAccessMgr::budget(const QString& _id) const { return m_budgetList[_id]; } void MyMoneySeqAccessMgr::removeBudget(const MyMoneyBudget& budget) { QMap::ConstIterator it; it = m_budgetList.find(budget.id()); if (it == m_budgetList.end()) { QString msg = "Unknown budget '" + budget.id() + '\''; throw MYMONEYEXCEPTION(msg); } m_budgetList.remove(budget.id()); } void MyMoneySeqAccessMgr::addPrice(const MyMoneyPrice& price) { MyMoneySecurityPair pricePair(price.from(), price.to()); QMap::ConstIterator it_m; it_m = m_priceList.find(pricePair); MyMoneyPriceEntries entries; if (it_m != m_priceList.end()) { entries = (*it_m); } // entries contains the current entries for this security pair // in case it_m points to m_priceList.end() we need to create a // new entry in the priceList, otherwise we need to modify // an existing one. MyMoneyPriceEntries::ConstIterator it; it = entries.constFind(price.date()); if (it != entries.constEnd()) { if ((*it).rate(QString()) == price.rate(QString()) && (*it).source() == price.source()) // in case the information did not change, we don't do anything return; } // store new value in local copy entries[price.date()] = price; if (it_m != m_priceList.end()) { m_priceList.modify(pricePair, entries); } else { m_priceList.insert(pricePair, entries); } } void MyMoneySeqAccessMgr::removePrice(const MyMoneyPrice& price) { MyMoneySecurityPair pricePair(price.from(), price.to()); QMap::ConstIterator it_m; it_m = m_priceList.find(pricePair); MyMoneyPriceEntries entries; if (it_m != m_priceList.end()) { entries = (*it_m); } // store new value in local copy entries.remove(price.date()); if (entries.count() != 0) { m_priceList.modify(pricePair, entries); } else { m_priceList.remove(pricePair); } } const MyMoneyPriceList MyMoneySeqAccessMgr::priceList() const { MyMoneyPriceList list; m_priceList.map(list); return list; } MyMoneyPrice MyMoneySeqAccessMgr::price(const QString& fromId, const QString& toId, const QDate& _date, const bool exactDate) const { // if the caller selected an exact entry, we can search for it using the date as the key QMap::const_iterator itm = m_priceList.find(qMakePair(fromId, toId)); if (itm != m_priceList.end()) { // if no valid date is passed, we use today's date. const QDate &date = _date.isValid() ? _date : QDate::currentDate(); const MyMoneyPriceEntries &entries = itm.value(); // regardless of the exactDate flag if the exact date is present return it's value since it's the correct value MyMoneyPriceEntries::const_iterator it = entries.find(date); if (it != entries.end()) return it.value(); // the exact date was not found look for the latest date before the requested date if the flag allows it if (!exactDate && !entries.empty()) { // if there are entries get the lower bound of the date it = entries.lowerBound(date); // since lower bound returns the first item with a larger key (we already know that key is not present) // if it's not the first item then we need to return the previous item (the map is not empty so there is one) if (it != entries.begin()) { return (--it).value(); } } } return MyMoneyPrice(); } void MyMoneySeqAccessMgr::rebuildAccountBalances() { // reset the balance of all accounts to 0 QMap map; m_accountList.map(map); QMap::iterator it_a; for (it_a = map.begin(); it_a != map.end(); ++it_a) { (*it_a).setBalance(MyMoneyMoney()); } // now scan over all transactions and all splits and setup the balances QMap::const_iterator it_t; for (it_t = m_transactionList.begin(); it_t != m_transactionList.end(); ++it_t) { const QList& splits = (*it_t).splits(); QList::const_iterator it_s = splits.begin(); for (; it_s != splits.end(); ++it_s) { if (!(*it_s).shares().isZero()) { const QString& id = (*it_s).accountId(); // locate the account and if present, update data if (map.find(id) != map.end()) { map[id].adjustBalance(*it_s); } } } } m_accountList = map; } bool MyMoneySeqAccessMgr::isReferenced(const MyMoneyObject& obj, const QBitArray& skipCheck) const { Q_ASSERT(skipCheck.count() == (int)Reference::Count); // We delete all references in reports when an object // is deleted, so we don't need to check here. See // MyMoneySeqAccessMgr::removeReferences(). In case // you miss the report checks in the following lines ;) const auto& id = obj.id(); // FIXME optimize the list of objects we have to checks // with a bit of knowledge of the internal structure, we // could optimize the number of objects we check for references // Scan all engine objects for a reference if (!skipCheck.testBit((int)Reference::Transaction)) foreach (const auto it, m_transactionList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Account)) foreach (const auto it, m_accountList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Institution)) foreach (const auto it, m_institutionList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Payee)) foreach (const auto it, m_payeeList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Tag)) foreach (const auto it, m_tagList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Budget)) foreach (const auto it, m_budgetList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Schedule)) foreach (const auto it, m_scheduleList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Security)) foreach (const auto it, m_securitiesList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Currency)) foreach (const auto it, m_currencyList) if (it.hasReferenceTo(id)) return true; // within the pricelist we don't have to scan each entry. Checking the QPair // members of the MyMoneySecurityPair is enough as they are identical to the // two security ids if (!skipCheck.testBit((int)Reference::Price)) { for (auto it_pr = m_priceList.begin(); it_pr != m_priceList.end(); ++it_pr) { if ((it_pr.key().first == id) || (it_pr.key().second == id)) return true; } } return false; } void MyMoneySeqAccessMgr::startTransaction() { m_payeeList.startTransaction(&m_nextPayeeID); m_tagList.startTransaction(&m_nextTagID); m_institutionList.startTransaction(&m_nextInstitutionID); m_accountList.startTransaction(&m_nextPayeeID); m_transactionList.startTransaction(&m_nextTransactionID); m_transactionKeys.startTransaction(); m_scheduleList.startTransaction(&m_nextScheduleID); m_securitiesList.startTransaction(&m_nextSecurityID); m_currencyList.startTransaction(); m_reportList.startTransaction(&m_nextReportID); m_budgetList.startTransaction(&m_nextBudgetID); m_priceList.startTransaction(); m_onlineJobList.startTransaction(&m_nextOnlineJobID); } bool MyMoneySeqAccessMgr::commitTransaction() { bool rc = false; rc |= m_payeeList.commitTransaction(); rc |= m_tagList.commitTransaction(); rc |= m_institutionList.commitTransaction(); rc |= m_accountList.commitTransaction(); rc |= m_transactionList.commitTransaction(); rc |= m_transactionKeys.commitTransaction(); rc |= m_scheduleList.commitTransaction(); rc |= m_securitiesList.commitTransaction(); rc |= m_currencyList.commitTransaction(); rc |= m_reportList.commitTransaction(); rc |= m_budgetList.commitTransaction(); rc |= m_priceList.commitTransaction(); rc |= m_onlineJobList.commitTransaction(); // if there was a change, touch the whole storage object if (rc) touch(); return rc; } void MyMoneySeqAccessMgr::rollbackTransaction() { m_payeeList.rollbackTransaction(); m_tagList.rollbackTransaction(); m_institutionList.rollbackTransaction(); m_accountList.rollbackTransaction(); m_transactionList.rollbackTransaction(); m_transactionKeys.rollbackTransaction(); m_scheduleList.rollbackTransaction(); m_securitiesList.rollbackTransaction(); m_currencyList.rollbackTransaction(); m_reportList.rollbackTransaction(); m_budgetList.rollbackTransaction(); m_priceList.rollbackTransaction(); m_onlineJobList.rollbackTransaction(); } void MyMoneySeqAccessMgr::removeReferences(const QString& id) { QMap::const_iterator it_r; QMap::const_iterator it_b; // remove from reports for (it_r = m_reportList.begin(); it_r != m_reportList.end(); ++it_r) { MyMoneyReport r = *it_r; r.removeReference(id); m_reportList.modify(r.id(), r); } // remove from budgets for (it_b = m_budgetList.begin(); it_b != m_budgetList.end(); ++it_b) { MyMoneyBudget b = *it_b; b.removeReference(id); m_budgetList.modify(b.id(), b); } } #undef TRY #undef CATCH #undef PASS diff --git a/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp b/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp index 25380d80e..e18edd7fd 100644 --- a/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp +++ b/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp @@ -1,225 +1,225 @@ /*************************************************************************** kofxdirectconnectdlg.cpp ------------------- begin : Sat Nov 13 2004 copyright : (C) 2002 by Ace Jones email : acejones@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 "kofxdirectconnectdlg.h" #include "kmymoneysettings.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyofxconnector.h" class KOfxDirectConnectDlg::Private { public: Private() : m_firstData(true) {} QFile m_fpTrace; bool m_firstData; }; KOfxDirectConnectDlg::KOfxDirectConnectDlg(const MyMoneyAccount& account, QWidget *parent) : KOfxDirectConnectDlgDecl(parent), d(new Private), m_tmpfile(0), m_connector(account), m_job(0) { } KOfxDirectConnectDlg::~KOfxDirectConnectDlg() { if (d->m_fpTrace.isOpen()) { d->m_fpTrace.close(); } delete m_tmpfile; delete d; } bool KOfxDirectConnectDlg::init() { show(); QByteArray request = m_connector.statementRequest(); if (request.isEmpty()) { hide(); return false; } // For debugging, dump out the request #if 0 QFile g("request.ofx"); g.open(QIODevice::WriteOnly); QTextStream(&g) << m_connector.url() << "\n" << QString(request); g.close(); #endif if (KMyMoneySettings::logOfxTransactions()) { QString logPath = KMyMoneySettings::logPath(); d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath)); d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append); } if (d->m_fpTrace.isOpen()) { QByteArray data = m_connector.url().toUtf8(); d->m_fpTrace.write("url: ", 5); d->m_fpTrace.write(data, strlen(data)); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("request:\n", 9); QByteArray trcData(request); // make local copy - trcData.replace('\r', ""); + trcData.replace('\r', '\0'); d->m_fpTrace.write(trcData, trcData.size()); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("response:\n", 10); } qDebug("creating job"); m_job = KIO::http_post(QUrl(m_connector.url()), request, KIO::HideProgressInfo); // open the temp file. We come around here twice if init() is called twice if (m_tmpfile) { qDebug() << "Already connected, using " << m_tmpfile->fileName(); delete m_tmpfile; //delete otherwise we mem leak } m_tmpfile = new QTemporaryFile(); // for debugging purposes one might want to leave the temp file around // in order to achieve this, please uncomment the next line // m_tmpfile->setAutoRemove(false); if (!m_tmpfile->open()) { qWarning("Unable to open tempfile '%s' for download.", qPrintable(m_tmpfile->fileName())); return false; } m_job->addMetaData("content-type", "Content-type: application/x-ofx"); connect(m_job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*))); connect(m_job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray))); setStatus(QString("Contacting %1...").arg(m_connector.url())); kProgress1->setMaximum(3); kProgress1->setValue(1); return true; } void KOfxDirectConnectDlg::setStatus(const QString& _status) { textLabel1->setText(_status); qDebug() << "STATUS:" << _status; } void KOfxDirectConnectDlg::setDetails(const QString& _details) { qDebug() << "DETAILS: " << _details; } void KOfxDirectConnectDlg::slotOfxData(KIO::Job*, const QByteArray& _ba) { qDebug("Got %d bytes of data", _ba.size()); if (d->m_firstData) { setStatus("Connection established, retrieving data..."); setDetails(QString("Downloading data to %1...").arg(m_tmpfile->fileName())); kProgress1->setValue(kProgress1->value() + 1); d->m_firstData = false; } m_tmpfile->write(_ba); setDetails(QString("Got %1 bytes").arg(_ba.size())); if (d->m_fpTrace.isOpen()) { QByteArray trcData(_ba); - trcData.replace('\r', ""); + trcData.replace('\r', '\0'); d->m_fpTrace.write(trcData, trcData.size()); } } void KOfxDirectConnectDlg::slotOfxFinished(KJob* /* e */) { qDebug("Job finished"); kProgress1->setValue(kProgress1->value() + 1); setStatus("Completed."); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write("\nCompleted\n\n\n\n", 14); } int error = m_job->error(); if (m_tmpfile) { qDebug("Closing tempfile"); m_tmpfile->close(); } qDebug("Tempfile closed"); if (error) { qDebug("Show error message"); m_job->uiDelegate()->showErrorMessage(); } else if (m_job->isErrorPage()) { qDebug("Process error page"); QString details; if (m_tmpfile) { QFile f(m_tmpfile->fileName()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); QString line; while (!stream.atEnd()) { details += stream.readLine(); // line of text excluding '\n' } f.close(); qDebug() << "The HTTP request failed: " << details; } } KMessageBox::detailedSorry(this, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed")); } else if (m_tmpfile) { qDebug("Emit statementReady signal with '%s'", qPrintable(m_tmpfile->fileName())); emit statementReady(m_tmpfile->fileName()); qDebug("Return from signal statementReady() processing"); } delete m_tmpfile; m_tmpfile = 0; hide(); qDebug("Finishing slotOfxFinished"); } void KOfxDirectConnectDlg::reject() { if (m_job) m_job->kill(); if (m_tmpfile) { m_tmpfile->close(); delete m_tmpfile; m_tmpfile = 0; } QDialog::reject(); } diff --git a/kmymoney/plugins/sqlcipher/tests/sqlcipherdrivertest.cpp b/kmymoney/plugins/sqlcipher/tests/sqlcipherdrivertest.cpp index b87f5da29..9bea08860 100644 --- a/kmymoney/plugins/sqlcipher/tests/sqlcipherdrivertest.cpp +++ b/kmymoney/plugins/sqlcipher/tests/sqlcipherdrivertest.cpp @@ -1,242 +1,242 @@ /* * QSqlDriver for SQLCipher * Copyright 2014 Christian David * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "sqlcipherdrivertest.h" #include #include #include #include #include #include QTEST_GUILESS_MAIN(sqlcipherdrivertest); static const QString passphrase = QLatin1String("blue valentines"); void sqlcipherdrivertest::initTestCase() { // Called before the first testfunction is executed QVERIFY(file.open()); file.close(); } void sqlcipherdrivertest::cleanupTestCase() { // Called after the last testfunction was executed db.close(); } void sqlcipherdrivertest::init() { // Called before each testfunction is executed } void sqlcipherdrivertest::cleanup() { // Called after every testfunction } void sqlcipherdrivertest::createUnencryptedDatabase() { QTemporaryFile file; file.open(); file.close(); QSqlDatabase db = QSqlDatabase::addDatabase("SQLCIPHER"); QVERIFY(db.isValid()); db.setDatabaseName(file.fileName()); QVERIFY(db.open()); QSqlQuery query; QVERIFY(query.exec("SELECT count(*) FROM sqlite_master;")); QVERIFY(!query.lastError().isValid()); QVERIFY(query.next()); QCOMPARE(query.value(0), QVariant(0)); db.close(); } void sqlcipherdrivertest::createDatabase() { QSqlDatabase db = QSqlDatabase::addDatabase("SQLCIPHER"); QVERIFY(db.isValid()); db.setDatabaseName(file.fileName()); db.setPassword(passphrase); QVERIFY(db.open()); } void sqlcipherdrivertest::checkReadAccess() { QSqlQuery query; QVERIFY(query.exec("SELECT count(*) FROM sqlite_master;")); QVERIFY(!query.lastError().isValid()); } void sqlcipherdrivertest::createTable() { QSqlQuery query; QVERIFY(query.exec("CREATE TABLE test (" "id int PRIMARY_KEY," "text varchar(20)" ");" )); QVERIFY(!query.lastError().isValid()); } int sqlcipherdrivertest::data() { QTest::addColumn("id"); QTest::addColumn("text"); QTest::newRow("string Hello World") << 1 << "Hello World"; QTest::newRow("string 20 characters") << 2 << "ABCDEFGHIJKLMNOPQRST"; QTest::newRow("simple string") << 3 << "simple"; return 3; // return number of rows! } void sqlcipherdrivertest::write_data() { data(); } void sqlcipherdrivertest::write() { QFETCH(int, id); QFETCH(QString, text); QSqlQuery query; query.prepare("INSERT INTO test (id, text) " "VALUES (:id, :text);" ); query.bindValue(":id", id); query.bindValue(":text", text); QVERIFY(query.exec()); QVERIFY(!query.lastError().isValid()); QCOMPARE(query.numRowsAffected(), 1); } void sqlcipherdrivertest::count() { QSqlQuery query; QVERIFY(query.exec("SELECT count(id) FROM test;")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), data()); } void sqlcipherdrivertest::readData_data() { data(); } void sqlcipherdrivertest::readData() { QFETCH(int, id); QFETCH(QString, text); QSqlQuery query; query.prepare("SELECT id, text FROM test WHERE id = :id;"); query.bindValue(":id", QVariant::fromValue(id)); QVERIFY(query.exec()); QVERIFY(!query.lastError().isValid()); QVERIFY(query.next()); QSqlRecord record = query.record(); int idIndex = record.indexOf("id"); QVERIFY(idIndex != -1); int textIndex = record.indexOf("text"); QVERIFY(textIndex != -1); QCOMPARE(query.value(0).toInt(), id); QCOMPARE(query.value(1).toString(), text); QVERIFY(!query.next()); } /** * taken from @url http://www.keenlogics.com/qt-sqlite-driver-plugin-for-encrypted-databases-using-sqlcipher-windows/ * thank you! */ void sqlcipherdrivertest::isFileEncrpyted() { file.open(); // http://www.sqlite.org/fileformat.html#database_header QString header = file.read(16); QVERIFY(header != "SQLite format 3\000"); file.close(); } void sqlcipherdrivertest::checkForEncryption_data() { data(); } void sqlcipherdrivertest::checkForEncryption() { QFETCH(QString, text); QVERIFY(file.open()); QString line; QTextStream in(&file); while (!in.atEnd()) { line = in.readLine(); if (line.contains(text)) { QFAIL("Found unencrypted text in database file"); } } file.close(); } void sqlcipherdrivertest::reopenDatabase() { db.close(); db = QSqlDatabase::addDatabase("SQLCIPHER"); QVERIFY(db.isValid()); db.setDatabaseName(file.fileName()); QVERIFY(db.open()); QSqlQuery query; - query.prepare("PRAGMA key = '" + passphrase + "'"); + query.prepare("PRAGMA key = '" + passphrase + '\''); QVERIFY(query.exec()); QVERIFY(!query.lastError().isValid()); checkReadAccess(); } void sqlcipherdrivertest::readData2_data() { data(); } void sqlcipherdrivertest::readData2() { readData(); } diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp index 37453f7f8..96f124ae1 100644 --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -1,2168 +1,2168 @@ /*************************************************************************** querytable.cpp ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones (C) 2007 Sascha Pfau (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /**************************************************************************** Contains code from the func_xirr and related methods of financial.cpp - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under GPLv2 or later. *****************************************************************************/ /*************************************************************************** * * * 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 "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #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 { // **************************************************************************** // // CashFlowListItem implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** QDate CashFlowListItem::m_sToday = QDate::currentDate(); MyMoneyMoney CashFlowListItem::NPV(double _rate) const { double T = static_cast(m_sToday.daysTo(m_date)) / 365.0; MyMoneyMoney result(m_value.toDouble() / pow(1 + _rate, T), 100); //qDebug() << "CashFlowListItem::NPV( " << _rate << " ) == " << result; return result; } // **************************************************************************** // // CashFlowList implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** CashFlowListItem CashFlowList::mostRecent() const { CashFlowList dupe(*this); qSort(dupe); //qDebug() << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate); return dupe.back(); } MyMoneyMoney CashFlowList::NPV(double _rate) const { MyMoneyMoney result; const_iterator it_cash = constBegin(); while (it_cash != constEnd()) { result += (*it_cash).NPV(_rate); ++it_cash; } //qDebug() << "CashFlowList::NPV( " << _rate << " ) == " << result << "------------------------" << endl; return result; } double CashFlowList::calculateXIRR() const { double resultRate = 0.00001; double resultZero = 0.00000; //if ( args.count() > 2 ) // resultRate = calc->conv()->asFloat ( args[2] ).asFloat(); // check pairs and count >= 2 and guess > -1.0 //if ( args[0].count() != args[1].count() || args[1].count() < 2 || resultRate <= -1.0 ) // return Value::errorVALUE(); // define max epsilon static const double maxEpsilon = 1e-5; // max number of iterations static const int maxIter = 50; // Newton's method - try to find a res, with a accuracy of maxEpsilon double rateEpsilon, newRate, resultValue; int i = 0; bool contLoop; do { resultValue = xirrResult(resultRate); double resultDerive = xirrResultDerive(resultRate); //check what happens if xirrResultDerive is zero //Don't know if it is correct to dismiss the result if (resultDerive != 0) { newRate = resultRate - resultValue / resultDerive; } else { newRate = resultRate - resultValue; } rateEpsilon = fabs(newRate - resultRate); resultRate = newRate; contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); } while (contLoop && (++i < maxIter)); if (contLoop) return resultZero; return resultRate; } double CashFlowList::xirrResult(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000;//back().value().toDouble(); QList::const_iterator list_it = constBegin(); while (list_it != constEnd()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); if (e_i < 0) { res += val.toDouble() * pow(r, -e_i); } else { res += val.toDouble() / pow(r, e_i); } ++list_it; } return res; } double CashFlowList::xirrResultDerive(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000; QList::const_iterator list_it = constBegin(); while (list_it != constEnd()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); res -= e_i * val.toDouble() / pow(r, e_i + 1.0); ++list_it; } return res; } double CashFlowList::IRR() const { double result = 0.0; // set 'today', which is the most recent of all dates in the list CashFlowListItem::setToday(mostRecent().date()); result = calculateXIRR(); return result; } MyMoneyMoney CashFlowList::total() const { MyMoneyMoney result; const_iterator it_cash = constBegin(); while (it_cash != constEnd()) { result += (*it_cash).value(); ++it_cash; } return result; } void CashFlowList::dumpDebug() const { const_iterator it_item = constBegin(); while (it_item != constEnd()) { qDebug() << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); ++it_item; } } // **************************************************************************** // // 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 MyMoneyReport::eAccountByTopAccount: case MyMoneyReport::eEquityType: case MyMoneyReport::eAccountType: case MyMoneyReport::eInstitution: constructAccountTable(); m_columns << ctAccount; break; case MyMoneyReport::eAccount: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; case MyMoneyReport::ePayee: case MyMoneyReport::eTag: case MyMoneyReport::eMonth: case MyMoneyReport::eWeek: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; case MyMoneyReport::eCashFlow: 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 MyMoneyReport::eCashFlow: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case MyMoneyReport::eCategory: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case MyMoneyReport::eTopCategory: m_group << ctCategoryType << ctTopCategory; break; case MyMoneyReport::eTopAccount: m_group << ctTopAccount << ctAccount; break; case MyMoneyReport::eAccount: m_group << ctAccount; break; case MyMoneyReport::eAccountReconcile: m_group << ctAccount << ctReconcileFlag; break; case MyMoneyReport::ePayee: m_group << ctPayee; break; case MyMoneyReport::eTag: m_group << ctTag; break; case MyMoneyReport::eMonth: m_group << ctMonth; break; case MyMoneyReport::eWeek: m_group << ctWeek; break; case MyMoneyReport::eAccountByTopAccount: m_group << ctTopAccount; break; case MyMoneyReport::eEquityType: m_group << ctEquityType; break; case MyMoneyReport::eAccountType: m_group << ctType; break; case MyMoneyReport::eInstitution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION("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 MyMoneyReport::eAccountByTopAccount: case MyMoneyReport::eEquityType: case MyMoneyReport::eAccountType: case MyMoneyReport::eInstitution: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); if (qc & MyMoneyReport::eQCnumber) m_columns << ctNumber; if (qc & MyMoneyReport::eQCpayee) m_columns << ctPayee; if (qc & MyMoneyReport::eQCtag) m_columns << ctTag; if (qc & MyMoneyReport::eQCcategory) m_columns << ctCategory; if (qc & MyMoneyReport::eQCaccount) m_columns << ctAccount; if (qc & MyMoneyReport::eQCreconciled) m_columns << ctReconcileFlag; if (qc & MyMoneyReport::eQCmemo) m_columns << ctMemo; if (qc & MyMoneyReport::eQCaction) m_columns << ctAction; if (qc & MyMoneyReport::eQCshares) m_columns << ctShares; if (qc & MyMoneyReport::eQCprice) m_columns << ctPrice; if (qc & MyMoneyReport::eQCperformance) { m_subtotal.clear(); switch (m_config.investmentSum()) { case MyMoneyReport::eSumOwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; case MyMoneyReport::eSumOwned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; case MyMoneyReport::eSumSold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; case MyMoneyReport::eSumPeriod: default: m_columns << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; } } if (qc & MyMoneyReport::eQCcapitalgain) { m_subtotal.clear(); switch (m_config.investmentSum()) { case MyMoneyReport::eSumOwned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; case MyMoneyReport::eSumSold: 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 & MyMoneyReport::eQCloan) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } if (qc & MyMoneyReport::eQCbalance) 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; } // iterate over groups from the lowest to the highest to find group change for (int i = groups.count() - 1; i >= 0 ; --i) { // if any of groups from next row changes (or next row is the last row), then it's time to put totals row if (lastRow || m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(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)).toString(); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / (*currencyGrp).at(i + 1).value(ctBuys).abs()).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 } QString 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)).toString(); 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 } QString 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 MyMoneyReport::eCategory: case MyMoneyReport::eTopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case MyMoneyReport::ePayee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); break; case MyMoneyReport::eTag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); 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; } } // 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() & MyMoneyReport::eQCloan) { 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; if (it_split == myBegin) { 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::ActionBuyShares) && 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::ActionAmortization) { // put the payment in the "payment" column and convert to lowest fraction qA[ctPayee] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::ActionInterest) { // 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 afer 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'); } 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 MyMoneyReport::eCategory: case MyMoneyReport::eTopCategory: 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 (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.size()) qA[ctTag] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA[ctTag] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) { // 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(); qS[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); 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? QString delimiter; for (int i = 0; i < tagIdList.size(); i++) { qA[ctTag] += delimiter + file->tag(tagIdList[i]).name().simplified(); - delimiter = "+"; + delimiter = '+'; } 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 MyMoneyReport::eAccount: case MyMoneyReport::eTopAccount: break; // case MyMoneyReport::eCategory: // 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); QMap::const_iterator it_account, accts_end; for (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; } } MyMoneyMoney QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!buys.isZero() || !startingBal.isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); returnInvestment = returnInvestment.convert(10000); } else returnInvestment = MyMoneyMoney(); // if no investment then no return on investment return returnInvestment; } MyMoneyMoney QueryTable::helperIRR(const CashFlowList &all) const { MyMoneyMoney annualReturn; try { double irr = all.IRR(); #ifdef Q_CC_MSVC annualReturn = MyMoneyMoney(_isnan(irr) ? 0 : irr, 10000); #else annualReturn = MyMoneyMoney(std::isnan(irr) ? 0 : irr, 10000); #endif } catch (QString e) { qDebug() << e; } return annualReturn; } 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() & MyMoneyReport::eQCcapitalgain) { // 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) { // substract not-sold shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // substract 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) { // substract not-added/not-reinvested shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // substract 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() == MyMoneyReport::eSumOwned && !shList[BuysOfOwned].isZero()) || (report.investmentSum() == MyMoneyReport::eSumSold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || (report.investmentSum() == MyMoneyReport::eSumOwnedAndSold && (!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 MyMoneyReport::eSumOwnedAndSold: 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 MyMoneyReport::eSumOwned: 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 MyMoneyReport::eSumSold: 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 MyMoneyReport::eSumPeriod: 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; } MyMoneyMoney returnInvestment = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); MyMoneyMoney annualReturn = helperIRR(all); result[ctBuys] = buysTotal.toString(); result[ctReturn] = annualReturn.toString(); result[ctReturnInvestment] = returnInvestment.toString(); 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 MyMoneyReport::eSumOwned: { 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 + endingBal)/buysTotal.abs()).toString(); break; } case MyMoneyReport::eSumSold: 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 MyMoneyReport::eQCperformance: { 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 MyMoneyReport::eQCcapitalgain: 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() == MyMoneyReport::eQCperformance && 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()).toString(); 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()).toString(); 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() & MyMoneyReport::eQCloan) { 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::ActionBuyShares) && (*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 MyMoneyReport::eAccount: case MyMoneyReport::eTopAccount: break; // case MyMoneyReport::eCategory: // 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); QMap::const_iterator it_account, accts_end; for (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/views/kpayeesview.cpp b/kmymoney/views/kpayeesview.cpp index 4a4b1a7a9..0b3a359f8 100644 --- a/kmymoney/views/kpayeesview.cpp +++ b/kmymoney/views/kpayeesview.cpp @@ -1,995 +1,995 @@ /*************************************************************************** kpayeesview.cpp --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai ***************************************************************************/ /*************************************************************************** * * * 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 "kpayeesview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "mymoneyfile.h" #include "mymoneyprice.h" #include "mymoneytransactionfilter.h" #include "kmymoneyglobalsettings.h" #include "kmymoney.h" #include "models.h" #include "accountsmodel.h" #include "mymoneysecurity.h" #include "mymoneycontact.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons/icons.h" #include "transaction.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; // *** KPayeeListItem Implementation *** KPayeeListItem::KPayeeListItem(QListWidget *parent, const MyMoneyPayee& payee) : QListWidgetItem(parent, QListWidgetItem::UserType), m_payee(payee) { setText(payee.name()); // allow in column rename setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); } KPayeeListItem::~KPayeeListItem() { } // *** KPayeesView Implementation *** KPayeesView::KPayeesView(QWidget *parent) : QWidget(parent), m_contact(new MyMoneyContact(this)), m_needReload(false), m_needLoad(true), m_inSelection(false), m_allowEditing(true), m_payeeFilterType(0) { } KPayeesView::~KPayeesView() { if(!m_needLoad) { // remember the splitter settings for startup KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); grp.writeEntry("KPayeesViewSplitterSize", m_splitter->saveState()); grp.sync(); } } void KPayeesView::setDefaultFocus() { QTimer::singleShot(0, m_searchWidget, SLOT(setFocus())); } void KPayeesView::init() { m_needLoad = false; setupUi(this); m_filterProxyModel = new AccountNamesFilterProxyModel(this); m_filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); comboDefaultCategory->setModel(m_filterProxyModel); matchTypeCombo->addItem(i18nc("@item No matching", "No matching"), MyMoneyPayee::matchDisabled); matchTypeCombo->addItem(i18nc("@item Match Payees name partially", "Match Payees name (partial)"), MyMoneyPayee::matchName); matchTypeCombo->addItem(i18nc("@item Match Payees name exactly", "Match Payees name (exact)"), MyMoneyPayee::matchNameExact); matchTypeCombo->addItem(i18nc("@item Search match in list", "Match on a name listed below"), MyMoneyPayee::matchKey); // create the searchline widget // and insert it into the existing layout m_searchWidget = new KListWidgetSearchLine(this, m_payeesList); m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); m_payeesList->setContextMenuPolicy(Qt::CustomContextMenu); m_listTopHLayout->insertWidget(0, m_searchWidget); //load the filter type m_filterBox->addItem(i18nc("@item Show all payees", "All")); m_filterBox->addItem(i18nc("@item Show only used payees", "Used")); m_filterBox->addItem(i18nc("@item Show only unused payees", "Unused")); m_filterBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); KGuiItem newButtonItem(QString(), QIcon::fromTheme(g_Icons[Icon::ListAddUser]), i18n("Creates a new payee"), i18n("Use this to create a new payee.")); KGuiItem::assign(m_newButton, newButtonItem); m_newButton->setToolTip(newButtonItem.toolTip()); KGuiItem renameButtonItem(QString(), QIcon::fromTheme(g_Icons[Icon::UserProperties]), i18n("Rename the current selected payee"), i18n("Use this to start renaming the selected payee.")); KGuiItem::assign(m_renameButton, renameButtonItem); m_renameButton->setToolTip(renameButtonItem.toolTip()); KGuiItem deleteButtonItem(QString(), QIcon::fromTheme(g_Icons[Icon::ListRemoveUser]), i18n("Delete selected payee(s)"), i18n("Use this to delete the selected payee. You can also select " "multiple payees to be deleted.")); KGuiItem::assign(m_deleteButton, deleteButtonItem); m_deleteButton->setToolTip(deleteButtonItem.toolTip()); KGuiItem mergeButtonItem(QString(), QIcon::fromTheme(g_Icons[Icon::Merge]), i18n("Merge multiple selected payees"), i18n("Use this to merge multiple selected payees.")); KGuiItem::assign(m_mergeButton, mergeButtonItem); m_mergeButton->setToolTip(mergeButtonItem.toolTip()); KGuiItem updateButtonItem(i18nc("Update payee", "Update"), QIcon::fromTheme(g_Icons[Icon::DialogOK]), i18n("Accepts the entered data and stores it"), i18n("Use this to accept the modified data.")); KGuiItem::assign(m_updateButton, updateButtonItem); KGuiItem syncButtonItem(i18nc("Sync payee", "Sync"), QIcon::fromTheme(g_Icons[Icon::Refresh]), i18n("Fetches the payee's data from your addressbook."), i18n("Use this to fetch payee's data.")); KGuiItem::assign(m_syncAddressbook, syncButtonItem); KGuiItem sendMailButtonItem(i18nc("Send mail", "Send"), QIcon::fromTheme(g_Icons[Icon::MailMessage]), i18n("Creates new e-mail to your payee."), i18n("Use this to create new e-mail to your payee.")); KGuiItem::assign(m_sendMail, sendMailButtonItem); m_updateButton->setEnabled(false); m_syncAddressbook->setEnabled(false); #ifndef KMM_ADDRESSBOOK_FOUND m_syncAddressbook->hide(); #endif matchTypeCombo->setCurrentIndex(0); checkMatchIgnoreCase->setEnabled(false); checkEnableDefaultCategory->setChecked(false); labelDefaultCategory->setEnabled(false); comboDefaultCategory->setEnabled(false); QList cols { eWidgets::eTransaction::Column::Date, eWidgets::eTransaction::Column::Account, eWidgets::eTransaction::Column::Detail, eWidgets::eTransaction::Column::ReconcileFlag, eWidgets::eTransaction::Column::Payment, eWidgets::eTransaction::Column::Deposit}; m_register->setupRegister(MyMoneyAccount(), cols); m_register->setSelectionMode(QTableWidget::SingleSelection); m_register->setDetailsColumnType(eWidgets::eRegister::DetailColumn::AccountFirst); m_balanceLabel->hide(); connect(m_contact, SIGNAL(contactFetched(ContactData)), this, SLOT(slotContactFetched(ContactData))); connect(m_payeesList, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(slotSelectPayee(QListWidgetItem*,QListWidgetItem*))); connect(m_payeesList, SIGNAL(itemSelectionChanged()), this, SLOT(slotSelectPayee())); connect(m_payeesList, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(slotStartRename(QListWidgetItem*))); connect(m_payeesList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(slotRenamePayee(QListWidgetItem*))); connect(m_payeesList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotOpenContextMenu(QPoint))); connect(m_renameButton, SIGNAL(clicked()), this, SLOT(slotRenameButtonCliked())); connect(m_deleteButton, SIGNAL(clicked()), kmymoney->actionCollection()->action(kmymoney->s_Actions[Action::PayeeDelete]), SLOT(trigger())); connect(m_mergeButton, SIGNAL(clicked()), kmymoney->actionCollection()->action(kmymoney->s_Actions[Action::PayeeMerge]), SLOT(trigger())); connect(m_newButton, SIGNAL(clicked()), this, SLOT(slotPayeeNew())); connect(addressEdit, SIGNAL(textChanged()), this, SLOT(slotPayeeDataChanged())); connect(postcodeEdit, SIGNAL(textChanged(QString)), this, SLOT(slotPayeeDataChanged())); connect(telephoneEdit, SIGNAL(textChanged(QString)), this, SLOT(slotPayeeDataChanged())); connect(emailEdit, SIGNAL(textChanged(QString)), this, SLOT(slotPayeeDataChanged())); connect(notesEdit, SIGNAL(textChanged()), this, SLOT(slotPayeeDataChanged())); connect(matchKeyEditList, SIGNAL(changed()), this, SLOT(slotKeyListChanged())); connect(matchTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPayeeDataChanged())); connect(checkMatchIgnoreCase, SIGNAL(toggled(bool)), this, SLOT(slotPayeeDataChanged())); connect(checkEnableDefaultCategory, SIGNAL(toggled(bool)), this, SLOT(slotPayeeDataChanged())); connect(comboDefaultCategory, SIGNAL(accountSelected(QString)), this, SLOT(slotPayeeDataChanged())); connect(buttonSuggestACategory, SIGNAL(clicked()), this, SLOT(slotChooseDefaultAccount())); connect(m_updateButton, SIGNAL(clicked()), this, SLOT(slotUpdatePayee())); connect(m_syncAddressbook, SIGNAL(clicked()), this, SLOT(slotSyncAddressBook())); connect(m_helpButton, SIGNAL(clicked()), this, SLOT(slotHelp())); connect(m_sendMail, SIGNAL(clicked()), this, SLOT(slotSendMail())); connect(m_register, SIGNAL(editTransaction()), this, SLOT(slotSelectTransaction())); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotLoadPayees())); connect(m_filterBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotChangeFilter(int))); connect(payeeIdentifiers, SIGNAL(dataChanged()), this, SLOT(slotPayeeDataChanged())); // use the size settings of the last run (if any) KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); m_splitter->restoreState(grp.readEntry("KPayeesViewSplitterSize", QByteArray())); m_splitter->setChildrenCollapsible(false); //At start we haven't any payee selected m_tabWidget->setEnabled(false); // disable tab widget m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons m_renameButton->setEnabled(false); m_mergeButton->setEnabled(false); m_payee = MyMoneyPayee(); // make sure we don't access an undefined payee clearItemData(); } void KPayeesView::slotChooseDefaultAccount() { MyMoneyFile* file = MyMoneyFile::instance(); QMap account_count; KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); while (item) { //only walk through selectable items. eg. transactions and not group markers if (item->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(item); MyMoneySplit s = t->transaction().splitByPayee(m_payee.id()); const MyMoneyAccount& acc = file->account(s.accountId()); QString txt; if (s.action() != MyMoneySplit::ActionAmortization && acc.accountType() != eMyMoney::Account::Type::AssetLoan && !file->isTransfer(t->transaction()) && t->transaction().splitCount() == 2) { MyMoneySplit s0 = t->transaction().splitByAccount(s.accountId(), false); if (account_count.contains(s0.accountId())) { account_count[s0.accountId()]++; } else { account_count[s0.accountId()] = 1; } } } item = item->nextItem(); } QMap::Iterator most_frequent, iter; most_frequent = account_count.begin(); for (iter = account_count.begin(); iter != account_count.end(); ++iter) { if (iter.value() > most_frequent.value()) { most_frequent = iter; } } if (most_frequent != account_count.end()) { checkEnableDefaultCategory->setChecked(true); comboDefaultCategory->setSelected(most_frequent.key()); setDirty(); } } void KPayeesView::slotStartRename(QListWidgetItem* item) { m_allowEditing = true; m_payeesList->editItem(item); } void KPayeesView::slotRenameButtonCliked() { if (m_payeesList->currentItem() && m_payeesList->selectedItems().count() == 1) { slotStartRename(m_payeesList->currentItem()); } } // This variant is only called when a single payee is selected and renamed. void KPayeesView::slotRenamePayee(QListWidgetItem* p) { //if there is no current item selected, exit if (m_allowEditing == false || !m_payeesList->currentItem() || p != m_payeesList->currentItem()) return; //qDebug() << "[KPayeesView::slotRenamePayee]"; // create a copy of the new name without appended whitespaces QString new_name = p->text(); if (m_payee.name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a payee with the new name try { // this function call will throw an exception, if the payee // hasn't been found. MyMoneyFile::instance()->payeeByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A payee with the name '%1' already exists. It is not advisable to have " "multiple payees with the same identification name. Are you sure you would like " "to rename the payee?", new_name)) != KMessageBox::Yes) { p->setText(m_payee.name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } m_payee.setName(new_name); m_newName = new_name; MyMoneyFile::instance()->modifyPayee(m_payee); // the above call to modifyPayee will reload the view so // all references and pointers to the view have to be // re-established. // make sure, that the record is visible even if it moved // out of sight due to the rename operation ensurePayeeVisible(m_payee.id()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to modify payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else { p->setText(new_name); } } void KPayeesView::ensurePayeeVisible(const QString& id) { for (int i = 0; i < m_payeesList->count(); ++i) { KPayeeListItem* p = dynamic_cast(m_payeesList->item(0)); if (p && p->payee().id() == id) { m_payeesList->scrollToItem(p, QAbstractItemView::PositionAtCenter); m_payeesList->setCurrentItem(p); // active item and deselect all others m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it break; } } } void KPayeesView::selectedPayees(QList& payeesList) const { QList selectedItems = m_payeesList->selectedItems(); QList::ConstIterator itemsIt = selectedItems.constBegin(); while (itemsIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*itemsIt); if (item) payeesList << item->payee(); ++itemsIt; } } void KPayeesView::slotSelectPayee(QListWidgetItem* cur, QListWidgetItem* prev) { Q_UNUSED(cur); Q_UNUSED(prev); m_allowEditing = false; } void KPayeesView::slotSelectPayee() { // check if the content of a currently selected payee was modified // and ask to store the data if (isDirty()) { QString question = QString("%1").arg(i18n("Do you want to save the changes for %1?", m_newName)); if (KMessageBox::questionYesNo(this, question, i18n("Save changes")) == KMessageBox::Yes) { m_inSelection = true; slotUpdatePayee(); m_inSelection = false; } } // make sure we always clear the selected list when listing again m_selectedPayeesList.clear(); // loop over all payees and count the number of payees, also // obtain last selected payee selectedPayees(m_selectedPayeesList); emit selectObjects(m_selectedPayeesList); if (m_selectedPayeesList.isEmpty()) { m_tabWidget->setEnabled(false); // disable tab widget m_balanceLabel->hide(); m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons m_renameButton->setEnabled(false); m_mergeButton->setEnabled(false); clearItemData(); m_payee = MyMoneyPayee(); m_syncAddressbook->setEnabled(false); return; // make sure we don't access an undefined payee } m_deleteButton->setEnabled(true); //re-enable delete button m_syncAddressbook->setEnabled(true); // if we have multiple payees selected, clear and disable the payee information if (m_selectedPayeesList.count() > 1) { m_tabWidget->setEnabled(false); // disable tab widget m_renameButton->setEnabled(false); // disable also the rename button m_mergeButton->setEnabled(true); m_balanceLabel->hide(); clearItemData(); } else { m_mergeButton->setEnabled(false); m_renameButton->setEnabled(true); } // otherwise we have just one selected, enable payee information widget m_tabWidget->setEnabled(true); m_balanceLabel->show(); // as of now we are updating only the last selected payee, and until // selection mode of the QListView has been changed to Extended, this // will also be the only selection and behave exactly as before - Andreas try { m_payee = m_selectedPayeesList[0]; m_newName = m_payee.name(); addressEdit->setEnabled(true); addressEdit->setText(m_payee.address()); postcodeEdit->setEnabled(true); postcodeEdit->setText(m_payee.postcode()); telephoneEdit->setEnabled(true); telephoneEdit->setText(m_payee.telephone()); emailEdit->setEnabled(true); emailEdit->setText(m_payee.email()); notesEdit->setText(m_payee.notes()); QStringList keys; bool ignorecase = false; MyMoneyPayee::payeeMatchType type = m_payee.matchData(ignorecase, keys); matchTypeCombo->setCurrentIndex(matchTypeCombo->findData(type)); matchKeyEditList->clear(); matchKeyEditList->insertStringList(keys); checkMatchIgnoreCase->setChecked(ignorecase); checkEnableDefaultCategory->setChecked(m_payee.defaultAccountEnabled()); comboDefaultCategory->setSelected(m_payee.defaultAccountId()); payeeIdentifiers->setSource(m_payee); slotPayeeDataChanged(); showTransactions(); } catch (const MyMoneyException &e) { qDebug("exception during display of payee: %s at %s:%ld", qPrintable(e.what()), qPrintable(e.file()), e.line()); m_register->clear(); m_selectedPayeesList.clear(); m_payee = MyMoneyPayee(); } m_allowEditing = true; } void KPayeesView::clearItemData() { addressEdit->setText(QString()); postcodeEdit->setText(QString()); telephoneEdit->setText(QString()); emailEdit->setText(QString()); notesEdit->setText(QString()); showTransactions(); } void KPayeesView::showTransactions() { MyMoneyMoney balance; MyMoneyFile *file = MyMoneyFile::instance(); MyMoneySecurity base = file->baseCurrency(); // setup sort order m_register->setSortOrder(KMyMoneyGlobalSettings::sortSearchView()); // clear the register m_register->clear(); if (m_selectedPayeesList.isEmpty() || !m_tabWidget->isEnabled()) { m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); return; } // setup the list and the pointer vector MyMoneyTransactionFilter filter; for (QList::const_iterator it = m_selectedPayeesList.constBegin(); it != m_selectedPayeesList.constEnd(); ++it) filter.addPayee((*it).id()); filter.setDateFilter(KMyMoneyGlobalSettings::startDate().date(), QDate()); // retrieve the list from the engine file->transactionList(m_transactionList, filter); // create the elements for the register QList >::const_iterator it; QMap uniqueMap; MyMoneyMoney deposit, payment; int splitCount = 0; bool balanceAccurate = true; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; MyMoneyAccount acc = file->account(split.accountId()); ++splitCount; uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); // take care of foreign currencies MyMoneyMoney val = split.shares().abs(); if (acc.currencyId() != base.id()) { const MyMoneyPrice &price = file->price(acc.currencyId(), base.id()); // in case the price is valid, we use it. Otherwise, we keep // a flag that tells us that the balance is somewhat inaccurate if (price.isValid()) { val *= price.rate(base.id()); } else { balanceAccurate = false; } } if (split.shares().isNegative()) { payment += val; } else { deposit += val; } } balance = deposit - payment; // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); m_register->updateRegister(true); // we might end up here with updates disabled on the register so // make sure that we enable updates here m_register->setUpdatesEnabled(true); m_balanceLabel->setText(i18n("Balance: %1%2", balanceAccurate ? "" : "~", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); } void KPayeesView::slotKeyListChanged() { bool rc = false; bool ignorecase = false; QStringList keys; m_payee.matchData(ignorecase, keys); if (matchTypeCombo->currentData().toUInt() == MyMoneyPayee::matchKey) { rc |= (keys != matchKeyEditList->items()); } setDirty(rc); } void KPayeesView::slotPayeeDataChanged() { bool rc = false; if (m_tabWidget->isEnabled()) { rc |= ((m_payee.email().isEmpty() != emailEdit->text().isEmpty()) || (!emailEdit->text().isEmpty() && m_payee.email() != emailEdit->text())); rc |= ((m_payee.address().isEmpty() != addressEdit->toPlainText().isEmpty()) || (!addressEdit->toPlainText().isEmpty() && m_payee.address() != addressEdit->toPlainText())); rc |= ((m_payee.postcode().isEmpty() != postcodeEdit->text().isEmpty()) || (!postcodeEdit->text().isEmpty() && m_payee.postcode() != postcodeEdit->text())); rc |= ((m_payee.telephone().isEmpty() != telephoneEdit->text().isEmpty()) || (!telephoneEdit->text().isEmpty() && m_payee.telephone() != telephoneEdit->text())); rc |= ((m_payee.name().isEmpty() != m_newName.isEmpty()) || (!m_newName.isEmpty() && m_payee.name() != m_newName)); rc |= ((m_payee.notes().isEmpty() != notesEdit->toPlainText().isEmpty()) || (!notesEdit->toPlainText().isEmpty() && m_payee.notes() != notesEdit->toPlainText())); bool ignorecase = false; QStringList keys; MyMoneyPayee::payeeMatchType type = m_payee.matchData(ignorecase, keys); rc |= (static_cast(type) != matchTypeCombo->currentData().toUInt()); checkMatchIgnoreCase->setEnabled(false); matchKeyEditList->setEnabled(false); if (matchTypeCombo->currentData().toUInt() != MyMoneyPayee::matchDisabled) { checkMatchIgnoreCase->setEnabled(true); // if we turn matching on, we default to 'ignore case' // TODO maybe make the default a user option if (type == MyMoneyPayee::matchDisabled && matchTypeCombo->currentData().toUInt() != MyMoneyPayee::matchDisabled) checkMatchIgnoreCase->setChecked(true); rc |= (ignorecase != checkMatchIgnoreCase->isChecked()); if (matchTypeCombo->currentData().toUInt() == MyMoneyPayee::matchKey) { matchKeyEditList->setEnabled(true); rc |= (keys != matchKeyEditList->items()); } } rc |= (checkEnableDefaultCategory->isChecked() != m_payee.defaultAccountEnabled()); if (checkEnableDefaultCategory->isChecked()) { comboDefaultCategory->setEnabled(true); labelDefaultCategory->setEnabled(true); // this is only going to understand the first in the list of selected accounts if (comboDefaultCategory->getSelected().isEmpty()) { rc |= !m_payee.defaultAccountId().isEmpty(); } else { QString temp = comboDefaultCategory->getSelected(); rc |= (temp.isEmpty() != m_payee.defaultAccountId().isEmpty()) || (!m_payee.defaultAccountId().isEmpty() && temp != m_payee.defaultAccountId()); } } else { comboDefaultCategory->setEnabled(false); labelDefaultCategory->setEnabled(false); } rc |= (m_payee.payeeIdentifiers() != payeeIdentifiers->identifiers()); } setDirty(rc); } void KPayeesView::slotUpdatePayee() { if (isDirty()) { MyMoneyFileTransaction ft; setDirty(false); try { m_payee.setName(m_newName); m_payee.setAddress(addressEdit->toPlainText()); m_payee.setPostcode(postcodeEdit->text()); m_payee.setTelephone(telephoneEdit->text()); m_payee.setEmail(emailEdit->text()); m_payee.setNotes(notesEdit->toPlainText()); m_payee.setMatchData(static_cast(matchTypeCombo->currentData().toUInt()), checkMatchIgnoreCase->isChecked(), matchKeyEditList->items()); m_payee.setDefaultAccountId(); m_payee.resetPayeeIdentifiers(payeeIdentifiers->identifiers()); if (checkEnableDefaultCategory->isChecked()) { QString temp; if (!comboDefaultCategory->getSelected().isEmpty()) { temp = comboDefaultCategory->getSelected(); m_payee.setDefaultAccountId(temp); } } MyMoneyFile::instance()->modifyPayee(m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to modify payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KPayeesView::slotSyncAddressBook() { if (m_payeeRows.isEmpty()) { // empty list means no syncing is pending... foreach (auto item, m_payeesList->selectedItems()) { m_payeeRows.append(m_payeesList->row(item)); // ...so initialize one } m_payeesList->clearSelection(); // otherwise slotSelectPayee will be run after every payee update // m_syncAddressbook->setEnabled(false); // disallow concurent syncs } if (m_payeeRows.count() <= m_payeeRow) { KPayeeListItem* item = dynamic_cast(m_payeesList->currentItem()); if (item) { // update ui if something is selected m_payee = item->payee(); addressEdit->setText(m_payee.address()); postcodeEdit->setText(m_payee.postcode()); telephoneEdit->setText(m_payee.telephone()); } m_payeeRows.clear(); // that means end of sync m_payeeRow = 0; return; } KPayeeListItem* item = dynamic_cast(m_payeesList->item(m_payeeRows.at(m_payeeRow))); if (item) m_payee = item->payee(); ++m_payeeRow; m_contact->fetchContact(m_payee.email()); // search for payee's data in addressbook and receive it in slotContactFetched } void KPayeesView::slotContactFetched(const ContactData &identity) { if (!identity.email.isEmpty()) { // empty e-mail means no identity fetched QString txt; if (!identity.street.isEmpty()) - txt.append(identity.street + "\n"); + txt.append(identity.street + '\n'); if (!identity.locality.isEmpty()) { txt.append(identity.locality); if (!identity.postalCode.isEmpty()) - txt.append(' ' + identity.postalCode + "\n"); + txt.append(' ' + identity.postalCode + '\n'); else - txt.append("\n"); + txt.append('\n'); } if (!identity.country.isEmpty()) - txt.append(identity.country + "\n"); + txt.append(identity.country + '\n'); if (!txt.isEmpty() && m_payee.address().compare(txt) != 0) m_payee.setAddress(txt); if (!identity.postalCode.isEmpty() && m_payee.postcode().compare(identity.postalCode) != 0) m_payee.setPostcode(identity.postalCode); if (!identity.phoneNumber.isEmpty() && m_payee.telephone().compare(identity.phoneNumber) != 0) m_payee.setTelephone(identity.phoneNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyPayee(m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to modify payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } slotSyncAddressBook(); // process next payee } void KPayeesView::slotSendMail() { QRegularExpression re(".+@.+"); if (re.match(m_payee.email()).hasMatch()) QDesktopServices::openUrl(QUrl(QStringLiteral("mailto:?to=") + m_payee.email(), QUrl::TolerantMode)); } void KPayeesView::showEvent(QShowEvent* event) { if (m_needLoad) init(); emit aboutToShow(); if (m_needReload) { loadPayees(); m_needReload = false; } // don't forget base class implementation QWidget::showEvent(event); QList list; selectedPayees(list); emit selectObjects(list); } void KPayeesView::slotLoadPayees() { if (isVisible()) { if (m_inSelection) QTimer::singleShot(0, this, SLOT(slotLoadPayees())); else loadPayees(); } else { m_needReload = true; } } void KPayeesView::loadPayees() { if (m_inSelection) return; QMap isSelected; QString id; MyMoneyFile* file = MyMoneyFile::instance(); // remember which items are selected in the list QList selectedItems = m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*payeesIt); if (item) isSelected[item->payee().id()] = true; ++payeesIt; } // keep current selected item KPayeeListItem *currentItem = static_cast(m_payeesList->currentItem()); if (currentItem) id = currentItem->payee().id(); m_allowEditing = false; // clear the list m_searchWidget->clear(); m_searchWidget->updateSearch(); m_payeesList->clear(); m_register->clear(); currentItem = 0; QListlist = file->payeeList(); QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (m_payeeFilterType == eAllPayees || (m_payeeFilterType == eReferencedPayees && file->isReferenced(*it)) || (m_payeeFilterType == eUnusedPayees && !file->isReferenced(*it))) { KPayeeListItem* item = new KPayeeListItem(m_payeesList, *it); if (item->payee().id() == id) currentItem = item; if (isSelected[item->payee().id()]) item->setSelected(true); } } m_payeesList->sortItems(); if (currentItem) { m_payeesList->setCurrentItem(currentItem); m_payeesList->scrollToItem(currentItem); } m_filterProxyModel->invalidate(); comboDefaultCategory->expandAll(); slotSelectPayee(0, 0); m_allowEditing = true; } void KPayeesView::slotSelectTransaction() { QList list = m_register->selectedItems(); if (!list.isEmpty()) { KMyMoneyRegister::Transaction* t = dynamic_cast(list[0]); if (t) emit transactionSelected(t->split().accountId(), t->transaction().id()); } } void KPayeesView::slotSelectPayeeAndTransaction(const QString& payeeId, const QString& accountId, const QString& transactionId) { if (!isVisible()) return; try { // clear filter m_searchWidget->clear(); m_searchWidget->updateSearch(); // deselect all other selected items QList selectedItems = m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*payeesIt); if (item) item->setSelected(false); ++payeesIt; } // find the payee in the list QListWidgetItem* it; for (int i = 0; i < m_payeesList->count(); ++i) { it = m_payeesList->item(i); KPayeeListItem* item = dynamic_cast(it); if (item && item->payee().id() == payeeId) { m_payeesList->scrollToItem(it, QAbstractItemView::PositionAtCenter); m_payeesList->setCurrentItem(it); // active item and deselect all others m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it //make sure the payee selection is updated and transactions are updated accordingly slotSelectPayee(); KMyMoneyRegister::RegisterItem *item = 0; for (int i = 0; i < m_register->rowCount(); ++i) { item = m_register->itemAtRow(i); KMyMoneyRegister::Transaction* t = dynamic_cast(item); if (t) { if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { m_register->selectItem(item); m_register->ensureItemVisible(item); break; } } } // quit out of for() loop break; } } } catch (const MyMoneyException &e) { qWarning("Unexpected exception in KPayeesView::slotSelectPayeeAndTransaction %s", qPrintable(e.what())); } } void KPayeesView::slotOpenContextMenu(const QPoint& /*p*/) { KPayeeListItem* item = dynamic_cast(m_payeesList->currentItem()); if (item) { slotSelectPayee(); emit openContextMenu(item->payee()); } } void KPayeesView::slotPayeeNew() { kmymoney->actionCollection()->action(kmymoney->s_Actions[Action::PayeeNew])->trigger(); } void KPayeesView::slotHelp() { KHelpClient::invokeHelp("details.payees"); } void KPayeesView::slotChangeFilter(int index) { //update the filter type then reload the payees list m_payeeFilterType = index; loadPayees(); } bool KPayeesView::isDirty() const { return m_updateButton->isEnabled(); } void KPayeesView::setDirty(bool dirty) { m_updateButton->setEnabled(dirty); } diff --git a/tools/xea2kmt.cpp b/tools/xea2kmt.cpp index 076f920ac..e77c76db3 100644 --- a/tools/xea2kmt.cpp +++ b/tools/xea2kmt.cpp @@ -1,653 +1,656 @@ /*************************************************************************** xea2kmt.cpp ------------------- copyright : (C) 2014 by Ralf Habacker ****************************************************************************/ /*************************************************************************** * * * 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 "../kmymoney/mymoney/mymoneyaccount.h" #include #include #include #include #include #include #include #include "mymoneyenums.h" using namespace eMyMoney; QDebug operator <<(QDebug out, const QXmlStreamNamespaceDeclaration &a) { out << "QXmlStreamNamespaceDeclaration(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << ")"; return out; } QDebug operator <<(QDebug out, const QXmlStreamAttribute &a) { out << "QXmlStreamAttribute(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << "name:" << a.name().toString() << " value:" << a.value().toString() << ")"; return out; } bool debug = false; bool withID = false; bool noLevel1Names = false; bool withTax = false; bool prefixNameWithCode = false; typedef QMap DirNameMapType; /** * map to hold differences from gnucash to kmymoney template directory * @return directory name map */ DirNameMapType &getDirNameMap() { static DirNameMapType dirNameMap; dirNameMap["cs"] = "cs_CZ"; dirNameMap["da"] = "dk"; dirNameMap["ja"] = "ja_JP"; dirNameMap["ko"] = "ko_KR"; dirNameMap["nb"] = "nb_NO"; dirNameMap["nl"] = "nl_NL"; dirNameMap["ru"] = "ru_RU"; return dirNameMap; } int toKMyMoneyAccountType(const QString &type) { if(type == "ROOT") return (int)Account::Type::Unknown; else if (type == "BANK") return (int)Account::Type::Checkings; else if (type == "CASH") return (int)Account::Type::Cash; else if (type == "CREDIT") return (int)Account::Type::CreditCard; else if (type == "INVEST") return (int)Account::Type::Investment; else if (type == "RECEIVABLE") return (int)Account::Type::Asset; else if (type == "ASSET") return (int)Account::Type::Asset; else if (type == "PAYABLE") return (int)Account::Type::Liability; else if (type == "LIABILITY") return (int)Account::Type::Liability; else if (type == "CURRENCY") return (int)Account::Type::Currency; else if (type == "INCOME") return (int)Account::Type::Income; else if (type == "EXPENSE") return (int)Account::Type::Expense; else if (type == "STOCK") return (int)Account::Type::Stock; else if (type == "MUTUAL") return (int)Account::Type::Stock; else if (type == "EQUITY") return (int)Account::Type::Equity; else return 99; // unknown } class TemplateAccount { public: typedef QList List; typedef QList PointerList; typedef QMap SlotList; QString id; QString type; QString name; QString code; QString parent; SlotList slotList; TemplateAccount() { } TemplateAccount(const TemplateAccount &b) : id(b.id), type(b.type), name(b.name), code(b.code), parent(b.parent), slotList(b.slotList) { } void clear() { id = ""; type = ""; name = ""; code = ""; parent = ""; slotList.clear(); } bool readSlots(QXmlStreamReader &xml) { while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef _name = xml.name(); if (_name == "slot") { type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef name = xml.name(); QString key, value; if (name == "key") key = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { name = xml.name(); if (name == "value") value = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); } if (!key.isEmpty() && !value.isEmpty()) slotList[key] = value; } } } else if (type == QXmlStreamReader::EndElement) { QStringRef _name = xml.name(); if (_name == "slots") return true; } } return true; } bool read(QXmlStreamReader &xml) { while (!xml.atEnd()) { xml.readNext(); QStringRef _name = xml.name(); if (xml.isEndElement() && _name == "account") { if (prefixNameWithCode && !code.isEmpty() && !name.startsWith(code)) - name = code + " " + name; + name = code + ' ' + name; return true; } if (xml.isStartElement()) { if (_name == "name") name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "id") id = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "type") type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "code") code = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "parent") parent = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "slots") readSlots(xml); else { if (debug) qDebug() << "skipping" << _name.toString(); } } } return false; } }; QDebug operator <<(QDebug out, const TemplateAccount &a) { out << "TemplateAccount(" << "name:" << a.name << "id:" << a.id << "type:" << a.type << "code:" << a.code << "parent:" << a.parent << "slotList:" << a.slotList << ")\n"; return out; } QDebug operator <<(QDebug out, const TemplateAccount::PointerList &a) { out << "TemplateAccount::List("; foreach(const TemplateAccount *account, a) out << *account; out << ")"; return out; } class TemplateFile { public: QString title; QString longDescription; QString shortDescription; TemplateAccount::List accounts; bool read(QXmlStreamReader &xml) { Q_ASSERT(xml.isStartElement() && xml.name() == "gnc-account-example"); while (xml.readNextStartElement()) { QStringRef name = xml.name(); if (xml.name() == "title") title = xml.readElementText().trimmed(); else if (xml.name() == "short-description") shortDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "long-description") longDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "account") { TemplateAccount account; if (account.read(xml)) accounts.append(account); } else { if (debug) qDebug() << "skipping" << name.toString(); xml.skipCurrentElement(); } } return true; } bool writeAsXml(QXmlStreamWriter &xml) { xml.writeStartElement("","title"); xml.writeCharacters(title); xml.writeEndElement(); xml.writeStartElement("","shortdesc"); xml.writeCharacters(shortDescription); xml.writeEndElement(); xml.writeStartElement("","longdesc"); xml.writeCharacters(longDescription); xml.writeEndElement(); xml.writeStartElement("","accounts"); bool result = writeAccountsAsXml(xml); xml.writeEndElement(); return result; } bool writeAccountsAsXml(QXmlStreamWriter &xml, const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { if (account->type != "ROOT") { xml.writeStartElement("","account"); xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->type))); xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->name); if (withID) xml.writeAttribute("id", account->id); if (withTax) { if (account->slotList.contains("tax-related")) { xml.writeStartElement("flag"); xml.writeAttribute("name","Tax"); xml.writeAttribute("value",account->slotList["tax-related"]); xml.writeEndElement(); } } } index++; writeAccountsAsXml(xml, account->id, index); index--; xml.writeEndElement(); } return true; } TemplateAccount *account(const QString &id) { for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.id == id) return &account; } return 0; } TemplateAccount::PointerList accountsByType(const QString &type) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.type == type) list.append(&account); } return list; } static bool nameLessThan(TemplateAccount *a1, TemplateAccount *a2) { return a1->name < a2->name; } TemplateAccount::PointerList accountsByParentID(const QString &parentID) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.parent == parentID) list.append(&account); } qSort(list.begin(), list.end(), nameLessThan); return list; } bool dumpTemplates(const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { QString a; a.fill(' ', index); qDebug() << a << account->name << toKMyMoneyAccountType(account->type); index++; dumpTemplates(account->id, index); index--; } return true; } }; QDebug operator <<(QDebug out, const TemplateFile &a) { out << "TemplateFile(" << "title:" << a.title << "short description:" << a.shortDescription << "long description:" << a.longDescription << "accounts:"; foreach(const TemplateAccount &account, a.accounts) out << account; out << ")"; return out; } class GnuCashAccountTemplateReader { public: GnuCashAccountTemplateReader() { } bool read(const QString &filename) { QFile file(filename); QTextStream in(&file); in.setCodec("utf-8"); if(!file.open(QIODevice::ReadOnly)) return false; inFileName = filename; return read(in.device()); } TemplateFile &result() { return _template; } bool dumpTemplates() { return _template.dumpTemplates(); } bool writeAsXml(const QString &filename=QString()) { if (filename.isEmpty()) { QTextStream stream(stdout); return writeAsXml(stream.device()); } else { QFile file(filename); if(!file.open(QIODevice::WriteOnly)) return false; return writeAsXml(&file); } } protected: bool checkAndUpdateAvailableNamespaces(QXmlStreamReader &xml) { if (xml.namespaceDeclarations().size() < 5) { qWarning() << "gnucash template file is missing required name space declarations; adding by self"; } xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("act", "http://www.gnucash.org/XML/act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc", "http://www.gnucash.org/XML/gnc")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc-act", "http://www.gnucash.org/XML/gnc-act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("cmdty","http://www.gnucash.org/XML/cmdty")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("slot","http://www.gnucash.org/XML/slot")); return true; } bool read(QIODevice *device) { xml.setDevice(device); while(!xml.atEnd()) { xml.readNext(); if (xml.isStartElement()) { if (xml.name() == "gnc-account-example") { checkAndUpdateAvailableNamespaces(xml); _template.read(xml); } else xml.raiseError(QObject::tr("The file is not an gnucash account template file.")); } } if (xml.error() != QXmlStreamReader::NoError) qWarning() << xml.errorString(); return !xml.error(); } bool writeAsXml(QIODevice *device) { QXmlStreamWriter xml(device); xml.setAutoFormatting(true); xml.setAutoFormattingIndent(1); xml.setCodec("utf-8"); xml.writeStartDocument(); QString fileName = inFileName; fileName.replace(QRegExp(".*/accounts"),"accounts"); xml.writeComment(QString("\n" " Converted using xea2kmt from GnuCash sources\n" "\n" " %1\n" "\n" " Please check the source file for possible copyright\n" " and license information.\n" ).arg(fileName)); xml.writeDTD(""); xml.writeStartElement("","kmymoney-account-template"); bool result = _template.writeAsXml(xml); xml.writeEndElement(); xml.writeEndDocument(); return result; } QXmlStreamReader xml; TemplateFile _template; QString inFileName; }; void scanDir(QDir dir, QStringList &files) { dir.setNameFilters(QStringList("*.gnucash-xea")); dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); if (debug) qDebug() << "Scanning: " << dir.path(); QStringList fileList = dir.entryList(); for (int i=0; i []"; qWarning() << argv[0] << " --in-dir --out-dir "; qWarning() << "options:"; qWarning() << " --debug - output debug information"; qWarning() << " --help - this page"; qWarning() << " --no-level1-names - do not export account names for top level accounts"; qWarning() << " --prefix-name-with-code - prefix account name with account code if present"; qWarning() << " --with-id - write account id attribute"; qWarning() << " --with-tax-related - parse and export gnucash 'tax-related' flag"; qWarning() << " --in-dir - search for gnucash templates files in "; qWarning() << " --out-dir - generate kmymoney templates below