diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index 6aadb6463..b10a9cfe3 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3508 +1,3508 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2001-2002 Felix Rodriguez * Copyright 2002-2004 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006-2018 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyfile.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "mymoneybalancecache.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "onlinejob.h" #include "storageenums.h" #include "mymoneyenums.h" // include the following line to get a 'cout' for debug purposes // #include using namespace eMyMoney; const QString MyMoneyFile::AccountSeparator = QChar(':'); MyMoneyFile MyMoneyFile::file; typedef QList > BalanceNotifyList; typedef QMap CacheNotifyList; /// @todo make this template based class MyMoneyNotification { public: MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) : m_objType(File::Object::Transaction), m_notificationMode(mode), m_id(t.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) : m_objType(File::Object::Account), m_notificationMode(mode), m_id(acc.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) : m_objType(File::Object::Institution), m_notificationMode(mode), m_id(institution.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) : m_objType(File::Object::Payee), m_notificationMode(mode), m_id(payee.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) : m_objType(File::Object::Tag), m_notificationMode(mode), m_id(tag.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) : m_objType(File::Object::Schedule), m_notificationMode(mode), m_id(schedule.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) : m_objType(File::Object::Security), m_notificationMode(mode), m_id(security.id()) { } MyMoneyNotification(File::Mode mode, const onlineJob& job) : m_objType(File::Object::OnlineJob), m_notificationMode(mode), m_id(job.id()) { } File::Object objectType() const { return m_objType; } File::Mode notificationMode() const { return m_notificationMode; } const QString& id() const { return m_id; } protected: MyMoneyNotification(File::Object obj, File::Mode mode, const QString& id) : m_objType(obj), m_notificationMode(mode), m_id(id) {} private: File::Object m_objType; File::Mode m_notificationMode; QString m_id; }; class MyMoneyFile::Private { public: Private() : m_storage(0), m_inTransaction(false) {} ~Private() { delete m_storage; } /** * This method is used to add an id to the list of objects * to be removed from the cache. If id is empty, then nothing is added to the list. * * @param id id of object to be notified * @param reload reload the object (@c true) or not (@c false). The default is @c true * @see attach, detach */ void addCacheNotification(const QString& id, const QDate& date) { if (!id.isEmpty()) m_balanceNotifyList.append(std::make_pair(id, date)); } /** * This method is used to clear the notification list */ void clearCacheNotification() { // reset list to be empty m_balanceNotifyList.clear(); } /** * This method is used to clear all * objects mentioned in m_notificationList from the cache. */ void notify() { foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) { m_balanceChangedSet += i.first; if (i.second.isValid()) { m_balanceCache.clear(i.first, i.second); } else { m_balanceCache.clear(i.first); } } clearCacheNotification(); } /** * This method checks if a storage object is attached and * throws and exception if not. */ inline void checkStorage() const { if (m_storage == 0) throw MYMONEYEXCEPTION_CSTRING("No storage object attached to MyMoneyFile"); } /** * This method checks that a transaction has been started with * startTransaction() and throws an exception otherwise. Calls * checkStorage() to make sure a storage object is present and attached. */ void checkTransaction(const char* txt) const { checkStorage(); if (!m_inTransaction) throw MYMONEYEXCEPTION(QString::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(txt))); } void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) { // get all affected accounts and add them to the m_valueChangedSet QList accList; file.accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { QString currencyId = account_it->currencyId(); if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) { // this account is not in the base currency and the price affects it's value m_valueChangedSet.insert(account_it->id()); } } } /** * This member points to the storage strategy */ MyMoneyStorageMgr *m_storage; bool m_inTransaction; MyMoneySecurity m_baseCurrency; /** * @brief Cache for MyMoneyObjects * * It is also used to emit the objectAdded() and objectModified() signals. * => If one of these signals is used, you must use this cache. */ MyMoneyPriceList m_priceCache; MyMoneyBalanceCache m_balanceCache; /** * This member keeps a list of account ids to notify * after a single operation is completed. The balance cache * is cleared for that account and all dates on or after * the one supplied. If the date is invalid, the entire * balance cache is cleared for that account. */ BalanceNotifyList m_balanceNotifyList; /** * This member keeps a list of account ids for which * a balanceChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_balanceChangedSet; /** * This member keeps a list of account ids for which * a valueChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_valueChangedSet; /** * This member keeps the list of changes in the engine * in historical order. The type can be 'added', 'modified' * or removed. */ QList m_changeSet; }; class MyMoneyNotifier { public: MyMoneyNotifier(MyMoneyFile::Private* file) { m_file = file; m_file->clearCacheNotification(); } ~MyMoneyNotifier() { m_file->notify(); } private: MyMoneyFile::Private* m_file; }; MyMoneyFile::MyMoneyFile() : d(new Private) { } MyMoneyFile::~MyMoneyFile() { delete d; } MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) : d(new Private) { attachStorage(storage); } MyMoneyFile* MyMoneyFile::instance() { return &file; } void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage) { if (d->m_storage != 0) throw MYMONEYEXCEPTION_CSTRING("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION_CSTRING("Storage must not be 0"); d->m_storage = storage; // force reload of base currency d->m_baseCurrency = MyMoneySecurity(); // and the whole cache d->m_balanceCache.clear(); d->m_priceCache.clear(); // notify application about new data availability emit beginChangeNotification(); emit dataChanged(); emit endChangeNotification(); } void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */) { d->m_balanceCache.clear(); d->m_priceCache.clear(); d->m_storage = nullptr; } MyMoneyStorageMgr* MyMoneyFile::storage() const { return d->m_storage; } bool MyMoneyFile::storageAttached() const { return d->m_storage != 0; } void MyMoneyFile::startTransaction() { d->checkStorage(); if (d->m_inTransaction) { throw MYMONEYEXCEPTION_CSTRING("Already started a transaction!"); } d->m_storage->startTransaction(); d->m_inTransaction = true; d->m_changeSet.clear(); } bool MyMoneyFile::hasTransaction() const { return d->m_inTransaction; } void MyMoneyFile::commitTransaction() { d->checkTransaction(Q_FUNC_INFO); // commit the transaction in the storage const auto changed = d->m_storage->commitTransaction(); d->m_inTransaction = false; // inform the outside world about the beginning of notifications emit beginChangeNotification(); // Now it's time to send out some signals to the outside world // First we go through the d->m_changeSet and emit respective // signals about addition, modification and removal of engine objects const auto& changes = d->m_changeSet; for (const auto& change : changes) { switch (change.notificationMode()) { case File::Mode::Remove: emit objectRemoved(change.objectType(), change.id()); // if there is a balance change recorded for this account remove it since the account itself will be removed // this can happen when deleting categories that have transactions and the reassign category feature was used d->m_balanceChangedSet.remove(change.id()); break; case File::Mode::Add: emit objectAdded(change.objectType(), change.id()); break; case File::Mode::Modify: emit objectModified(change.objectType(), change.id()); break; } } // we're done with the change set, so we clear it d->m_changeSet.clear(); // now send out the balanceChanged signal for all those // accounts for which we have an indication about a possible // change. const auto& balanceChanges = d->m_balanceChangedSet; for (const auto& id : balanceChanges) { // if we notify about balance change we don't need to notify about value change // for the same account since a balance change implies a value change d->m_valueChangedSet.remove(id); emit balanceChanged(account(id)); } d->m_balanceChangedSet.clear(); // now notify about the remaining value changes const auto& m_valueChanges = d->m_valueChangedSet; for (const auto& id : m_valueChanges) emit valueChanged(account(id)); d->m_valueChangedSet.clear(); // as a last action, send out the global dataChanged signal if (changed) emit dataChanged(); // inform the outside world about the end of notifications emit endChangeNotification(); } void MyMoneyFile::rollbackTransaction() { d->checkTransaction(Q_FUNC_INFO); d->m_storage->rollbackTransaction(); d->m_inTransaction = false; d->m_balanceChangedSet.clear(); d->m_valueChangedSet.clear(); d->m_changeSet.clear(); } void MyMoneyFile::addInstitution(MyMoneyInstitution& institution) { // perform some checks to see that the institution stuff is OK. For // now we assume that the institution must have a name, the ID is not set // and it does not have a parent (MyMoneyFile). if (institution.name().length() == 0 || institution.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("Not a new institution"); d->checkTransaction(Q_FUNC_INFO); d->m_storage->addInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution); } void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction tCopy(transaction); // now check the splits bool loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account"); if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))) loanAccountAffected = true; } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); tCopy.modifySplit(s); } } } } // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current setting of this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list // and mark all accounts that are referenced const auto splits2 = tr.splits(); foreach (const auto& split, splits2) d->addCacheNotification(split.accountId(), tr.postDate()); // make sure the value is rounded to the accounts precision fixSplitPrecision(tCopy); // perform modification d->m_storage->modifyTransaction(tCopy); // and mark all accounts that are referenced const auto splits3 = tCopy.splits(); for (const auto& split : splits3) d->addCacheNotification(split.accountId(), tCopy.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction); } void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount account(_account); auto acc = MyMoneyFile::account(account.id()); // check that for standard accounts only specific parameters are changed if (isStandardAccount(account.id())) { // make sure to use the stuff we found on file account = acc; // and only use the changes that are allowed account.setName(_account.name()); account.setCurrencyId(_account.currencyId()); // now check that it is the same if (!(account == _account)) throw MYMONEYEXCEPTION_CSTRING("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION_CSTRING("Unable to change account type"); // if the account was moved to another institution, we notify // the old one as well as the new one and the structure change if (acc.institutionId() != account.institutionId()) { MyMoneyInstitution inst; if (!acc.institutionId().isEmpty()) { inst = institution(acc.institutionId()); inst.removeAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } if (!account.institutionId().isEmpty()) { inst = institution(account.institutionId()); inst.addAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } } d->m_storage->modifyAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account); } void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); // check that it's not one of the standard account groups if (isStandardAccount(acc.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent the standard account groups"); if (acc.accountGroup() == parent.accountGroup() || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense) || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) { if (acc.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent Stock to non-investment account"); if (parent.accountType() == Account::Type::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent non-stock to investment account"); // keep a notification of the current parent MyMoneyAccount curParent = account(acc.parentAccountId()); d->m_storage->reparentAccount(acc, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } else throw MYMONEYEXCEPTION_CSTRING("Unable to reparent to different account type"); } MyMoneyInstitution MyMoneyFile::institution(const QString& id) const { return d->m_storage->institution(id); } MyMoneyAccount MyMoneyFile::account(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id return MyMoneyAccount(); return d->m_storage->account(id); } MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const { static MyMoneyAccount nullAccount; const auto accounts = account.accountList(); for (const auto& acc : accounts) { const auto sacc = MyMoneyFile::account(acc); if (sacc.name().compare(name) == 0) return sacc; } return nullAccount; } MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const { try { return d->m_storage->accountByName(name); } catch (const MyMoneyException &) { } return MyMoneyAccount(); } void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the engine's idea about this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list const auto splits = tr.splits(); for (const auto& split : splits) { auto acc = account(split.accountId()); if (acc.isClosed()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove transaction that references a closed account.")); d->addCacheNotification(split.accountId(), tr.postDate()); //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ?? } d->m_storage->removeTransaction(transaction); // remove a possible notification of that same object from the changeSet QList::iterator it; for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) { if((*it).id() == transaction.id()) { it = d->m_changeSet.erase(it); } else { ++it; } } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction); } bool MyMoneyFile::hasActiveSplits(const QString& id) const { d->checkStorage(); return d->m_storage->hasActiveSplits(id); } bool MyMoneyFile::isStandardAccount(const QString& id) const { d->checkStorage(); return d->m_storage->isStandardAccount(id); } void MyMoneyFile::setAccountName(const QString& id, const QString& name) const { d->checkTransaction(Q_FUNC_INFO); auto acc = account(id); d->m_storage->setAccountName(id, name); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } void MyMoneyFile::removeAccount(const MyMoneyAccount& account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent; MyMoneyAccount acc; MyMoneyInstitution institution; // check that the account and its parent exist // this will throw an exception if the id is unknown acc = MyMoneyFile::account(account.id()); parent = MyMoneyFile::account(account.parentAccountId()); if (!acc.institutionId().isEmpty()) institution = MyMoneyFile::institution(acc.institutionId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits"); } // collect all sub-ordinate accounts for notification const auto accounts = acc.accountList(); for (const auto& id : accounts) d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id)); // don't forget the parent and a possible institution if (!institution.id().isEmpty()) { institution.removeAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } acc.setInstitutionId(QString()); d->m_storage->removeAccount(acc); d->m_balanceCache.clear(acc.id()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc); } void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::removeAccountList]!"); d->checkTransaction(Q_FUNC_INFO); // upon entry, we check that we could proceed with the operation if (!level) { if (!hasOnlyUnusedAccounts(account_list, 0)) { throw MYMONEYEXCEPTION_CSTRING("One or more accounts cannot be removed"); } } // process all accounts in the list and test if they have transactions assigned foreach (const auto sAccount, account_list) { auto a = d->m_storage->account(sAccount); //qDebug() << "Deleting account '"<< a.name() << "'"; // first remove all sub-accounts if (!a.accountList().isEmpty()) { removeAccountList(a.accountList(), level + 1); // then remove account itself, but we first have to get // rid of the account list that is still stored in // the MyMoneyAccount object. Easiest way is to get a fresh copy. a = d->m_storage->account(sAccount); } // make sure to remove the item from the cache removeAccount(a); } } bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!"); // process all accounts in the list and test if they have transactions assigned for (const auto& sAccount : account_list) { if (transactionCount(sAccount) != 0) return false; // the current account has a transaction assigned if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1)) return false; // some sub-account has a transaction assigned } return true; // all subaccounts unused } void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution inst = MyMoneyFile::institution(institution.id()); bool blocked = signalsBlocked(); blockSignals(true); const auto accounts = inst.accountList(); for (const auto& acc : accounts) { auto a = account(acc); a.setInstitutionId(QString()); modifyAccount(a); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a); } blockSignals(blocked); d->m_storage->removeInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution); } void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(baseCurrency().id()); MyMoneyFileTransaction ft; try { int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); } addAccount(newAccount, parentAccount); // in case of a loan account, we add the initial payment if ((newAccount.accountType() == Account::Type::Loan || newAccount.accountType() == Account::Type::AssetLoan) && !newAccount.value("kmm-loan-payment-acc").isEmpty() && !newAccount.value("kmm-loan-payment-date").isEmpty()) { MyMoneyAccountLoan acc(newAccount); MyMoneyTransaction t; MyMoneySplit a, b; a.setAccountId(acc.id()); b.setAccountId(acc.value("kmm-loan-payment-acc").toLatin1()); a.setValue(acc.loanAmount()); if (acc.accountType() == Account::Type::Loan) a.setValue(-a.value()); a.setShares(a.value()); b.setValue(-a.value()); b.setShares(b.value()); a.setMemo(i18n("Loan payout")); b.setMemo(i18n("Loan payout")); t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate)); newAccount.deletePair("kmm-loan-payment-acc"); newAccount.deletePair("kmm-loan-payment-date"); MyMoneyFile::instance()->modifyAccount(newAccount); t.addSplit(a); t.addSplit(b); addTransaction(t); createOpeningBalanceTransaction(newAccount, openingBal); // in case of an investment account we check if we should create // a brokerage account } else if (newAccount.accountType() == Account::Type::Investment && !brokerageAccount.name().isEmpty()) { addAccount(brokerageAccount, parentAccount); // set a link from the investment account to the brokerage account modifyAccount(newAccount); createOpeningBalanceTransaction(brokerageAccount, openingBal); } else createOpeningBalanceTransaction(newAccount, openingBal); ft.commit(); } catch (const MyMoneyException &e) { qWarning("Unable to create account: %s", e.what()); throw; } } void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution institution; // perform some checks to see that the account stuff is OK. For // now we assume that the account must have a name, has no // transaction and sub-accounts and parent account // it's own ID is not set and it does not have a pointer to (MyMoneyFile) if (account.name().length() == 0) throw MYMONEYEXCEPTION_CSTRING("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id"); if (account.accountType() == Account::Type::Unknown) throw MYMONEYEXCEPTION_CSTRING("Account has invalid type"); // make sure, that the parent account exists // if not, an exception is thrown. If it exists, // get a copy of the current data auto acc = MyMoneyFile::account(parent.id()); #if 0 // TODO: remove the following code as we now can have multiple accounts // with the same name even in the same hierarchy position of the account tree // // check if the selected name is currently not among the child accounts // if we find one, then return it as the new account QStringList::const_iterator it_a; foreach (const auto accountID, acc.accountList()) { MyMoneyAccount a = MyMoneyFile::account(accountID); if (account.name() == a.name()) { account = a; return; } } #endif // FIXME: make sure, that the parent has the same type // I left it out here because I don't know, if there is // a tight coupling between e.g. checking accounts and the // class asset. It certainly does not make sense to create an // expense account under an income account. Maybe it does, I don't know. // We enforce, that a stock account can never be a parent and // that the parent for a stock account must be an investment. Also, // an investment cannot have another investment account as it's parent if (parent.isInvest()) throw MYMONEYEXCEPTION_CSTRING("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Investment account can only have stock accounts as children"); // if an institution is set, verify that it exists if (account.institutionId().length() != 0) { // check the presence of the institution. if it // does not exist, an exception is thrown institution = MyMoneyFile::institution(account.institutionId()); } // if we don't have a valid opening date use today if (!account.openingDate().isValid()) { account.setOpeningDate(QDate::currentDate()); } // make sure to set the opening date for categories to a // fixed date (1900-1-1). See #313793 on b.k.o for details if (account.isIncomeExpense()) { account.setOpeningDate(QDate(1900, 1, 1)); } // if we don't have a currency assigned use the base currency if (account.currencyId().isEmpty()) { account.setCurrencyId(baseCurrency().id()); } // make sure the parent id is setup account.setParentAccountId(parent.id()); d->m_storage->addAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Add, account); d->m_storage->addAccount(parent, account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); if (account.institutionId().length() != 0) { institution.addAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } } MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance) { MyMoneyTransaction t; // if the opening balance is not zero, we need // to create the respective transaction if (!balance.isZero()) { d->checkTransaction(Q_FUNC_INFO); MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc = openingBalanceAccount(currency); if (openAcc.openingDate() > acc.openingDate()) { openAcc.setOpeningDate(acc.openingDate()); modifyAccount(openAcc); } MyMoneySplit s; t.setPostDate(acc.openingDate()); t.setCommodity(acc.currencyId()); s.setAccountId(acc.id()); s.setShares(balance); s.setValue(balance); t.addSplit(s); s.clearId(); s.setAccountId(openAcc.id()); s.setShares(-balance); s.setValue(-balance); t.addSplit(s); addTransaction(t); } return t; } QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const { QString result; MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc; try { openAcc = openingBalanceAccount(currency); } catch (const MyMoneyException &) { return result; } // Iterate over all the opening balance transactions for this currency MyMoneyTransactionFilter filter; filter.addAccount(openAcc.id()); QList transactions = transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account (*it_t).splitByAccount(acc.id(), true /*match*/); // If so, we have a winner! result = (*it_t).id(); break; } catch (const MyMoneyException &) { // If not, keep searching ++it_t; } } return result; } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) { if (!security.isCurrency()) throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported"); try { return openingBalanceAccount_internal(security); } catch (const MyMoneyException &) { MyMoneyFileTransaction ft; MyMoneyAccount acc; try { acc = createOpeningBalanceAccount(security); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to create opening balance account for security %s", qPrintable(security.id())); } return acc; } } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const { return openingBalanceAccount_internal(security); } MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const { if (!security.isCurrency()) throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported"); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && it->currencyId() == security.id()) { acc = *it; break; } } if (acc.id().isEmpty()) { for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) && it->currencyId() == security.id()) { acc = *it; break; } } } if (acc.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No opening balance account for %1").arg(security.tradingSymbol())); return acc; } MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); // find present opening balance accounts without containing '(' QString name; QString parentAccountId; QRegExp exp(QString("\\([A-Z]{3}\\)")); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && exp.indexIn(it->name()) == -1) { name = it->name(); parentAccountId = it->parentAccountId(); break; } } if (name.isEmpty()) name = MyMoneyFile::openingBalancesPrefix(); if (security.id() != baseCurrency().id()) { name += QString(" (%1)").arg(security.id()); } acc.setName(name); acc.setAccountType(Account::Type::Equity); acc.setCurrencyId(security.id()); acc.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity(); this->addAccount(acc, parent); return acc; } void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * no ids are assigned // * the date valid (must not be empty) // * the referenced accounts in the splits exist // first perform all the checks if (!transaction.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with invalid postdate"); // now check the splits auto loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account"); } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { foreach (const auto split, transaction.splits()) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); transaction.modifySplit(s); } } } } // check that we have a commodity if (transaction.commodity().isEmpty()) { transaction.setCommodity(baseCurrency().id()); } // make sure the value is rounded to the accounts precision fixSplitPrecision(transaction); // then add the transaction to the file global pool d->m_storage->addTransaction(transaction); // scan the splits again to update notification list const auto splits2 = transaction.splits(); for (const auto& split : splits2) d->addCacheNotification(split.accountId(), transaction.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction); } MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const { d->checkStorage(); return d->m_storage->transaction(id); } MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const { d->checkStorage(); return d->m_storage->transaction(account, idx); } void MyMoneyFile::addPayee(MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee); } MyMoneyPayee MyMoneyFile::payee(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return MyMoneyPayee(); return d->m_storage->payee(id); } MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const { d->checkStorage(); return d->m_storage->payeeByName(name); } void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee); } void MyMoneyFile::removePayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the payee is not referenced anymore d->m_storage->removePayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee); } void MyMoneyFile::addTag(MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag); } MyMoneyTag MyMoneyFile::tag(const QString& id) const { return d->m_storage->tag(id); } MyMoneyTag MyMoneyFile::tagByName(const QString& name) const { d->checkStorage(); return d->m_storage->tagByName(name); } void MyMoneyFile::modifyTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag); } void MyMoneyFile::removeTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the tag is not referenced anymore d->m_storage->removeTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag); } void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { if (idlist.isEmpty()) { d->m_storage->accountList(list); #if 0 // TODO: I have no idea what this was good for, but it caused the networth report // to show double the numbers so I commented it out (ipwizard, 2008-05-24) if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) { d->m_storage->accountList(list); d->m_cache.preloadAccount(list); } #endif QList::Iterator it; for (it = list.begin(); it != list.end();) { if (isStandardAccount((*it).id())) { it = list.erase(it); } else { ++it; } } } else { QList::ConstIterator it; QList list_a; d->m_storage->accountList(list_a); for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) { if (!isStandardAccount((*it).id())) { if (idlist.indexOf((*it).id()) != -1) { list.append(*it); if (recursive == true && !(*it).accountList().isEmpty()) { accountList(list, (*it).accountList(), true); } } } } } } QList MyMoneyFile::institutionList() const { return d->m_storage->institutionList(); } // general get functions MyMoneyPayee MyMoneyFile::user() const { d->checkStorage(); return d->m_storage->user(); } // general set functions void MyMoneyFile::setUser(const MyMoneyPayee& user) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setUser(user); } bool MyMoneyFile::dirty() const { if (!d->m_storage) return false; return d->m_storage->dirty(); } void MyMoneyFile::setDirty() const { d->checkStorage(); d->m_storage->setDirty(); } unsigned int MyMoneyFile::accountCount() const { d->checkStorage(); return d->m_storage->accountCount(); } void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const { if (acc.currencyId().isEmpty()) { if (!baseCurrency().id().isEmpty()) acc.setCurrencyId(baseCurrency().id()); } } MyMoneyAccount MyMoneyFile::liability() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)); } MyMoneyAccount MyMoneyFile::asset() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)); } MyMoneyAccount MyMoneyFile::expense() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); } MyMoneyAccount MyMoneyFile::income() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)); } MyMoneyAccount MyMoneyFile::equity() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)); } unsigned int MyMoneyFile::transactionCount(const QString& account) const { d->checkStorage(); return d->m_storage->transactionCount(account); } unsigned int MyMoneyFile::transactionCount() const { return transactionCount(QString()); } QMap MyMoneyFile::transactionCountMap() const { d->checkStorage(); return d->m_storage->transactionCountMap(); } unsigned int MyMoneyFile::institutionCount() const { d->checkStorage(); return d->m_storage->institutionCount(); } MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const { if (date.isValid()) { MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date); if (bal.isValid()) return bal.balance(); } d->checkStorage(); MyMoneyMoney returnValue = d->m_storage->balance(id, date); if (date.isValid()) { d->m_balanceCache.insert(id, date, returnValue); } return returnValue; } MyMoneyMoney MyMoneyFile::balance(const QString& id) const { return balance(id, QDate()); } MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const { MyMoneyMoney cleared; QList list; cleared = balance(id, date); MyMoneyAccount account = this->account(id); MyMoneyMoney factor(1, 1); if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity) factor = -factor; MyMoneyTransactionFilter filter; filter.addAccount(id); filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); filter.addState((int)TransactionFilter::State::NotReconciled); transactionList(list, filter); for (QList::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); for (QList::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { const MyMoneySplit &split = (*it_s); if (split.accountId() != id) continue; cleared -= split.shares(); } } return cleared * factor; } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const { d->checkStorage(); return d->m_storage->totalBalance(id, date); } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const { return totalBalance(id, QDate()); } void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const { MyMoneySecurity from, to; try { from = security(fromId); to = security(toId); qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name())); } catch (const MyMoneyException &e) { qWarning("Missing security caught in MyMoneyFile::warningMissingRate(). %s", e.what()); } } void MyMoneyFile::transactionList(QList >& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } void MyMoneyFile::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } QList MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const { d->checkStorage(); return d->m_storage->transactionList(filter); } QList MyMoneyFile::payeeList() const { return d->m_storage->payeeList(); } QList MyMoneyFile::tagList() const { return d->m_storage->tagList(); } QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; QString rc; if (!accountId.isEmpty()) { acc = account(accountId); do { if (!rc.isEmpty()) rc = AccountSeparator + rc; rc = acc.name() + rc; acc = account(acc.parentAccountId()); } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id()))); } return rc; } QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const { QString id; // search the category in the expense accounts and if it is not found, try // to locate it in the income accounts if (type == Account::Type::Unknown || type == Account::Type::Expense) { id = locateSubAccount(MyMoneyFile::instance()->expense(), category); } if ((id.isEmpty() && type == Account::Type::Unknown) || type == Account::Type::Income) { id = locateSubAccount(MyMoneyFile::instance()->income(), category); } return id; } QString MyMoneyFile::categoryToAccount(const QString& category) const { return categoryToAccount(category, Account::Type::Unknown); } QString MyMoneyFile::nameToAccount(const QString& name) const { QString id; // search the category in the asset accounts and if it is not found, try // to locate it in the liability accounts id = locateSubAccount(MyMoneyFile::instance()->asset(), name); if (id.isEmpty()) id = locateSubAccount(MyMoneyFile::instance()->liability(), name); return id; } QString MyMoneyFile::parentName(const QString& name) const { return name.section(AccountSeparator, 0, -2); } QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const { MyMoneyAccount nextBase; QString level, remainder; level = category.section(AccountSeparator, 0, 0); remainder = category.section(AccountSeparator, 1); foreach (const auto sAccount, base.accountList()) { nextBase = account(sAccount); if (nextBase.name() == level) { if (remainder.isEmpty()) { return nextBase.id(); } return locateSubAccount(nextBase, remainder); } } return QString(); } QString MyMoneyFile::value(const QString& key) const { d->checkStorage(); return d->m_storage->value(key); } void MyMoneyFile::setValue(const QString& key, const QString& val) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setValue(key, val); } void MyMoneyFile::deletePair(const QString& key) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->deletePair(key); } void MyMoneyFile::addSchedule(MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); const auto splits = sched.transaction().splits(); for (const auto& split : splits) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts const auto acc = account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account"); } d->m_storage->addSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched); } void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); foreach (const auto split, sched.transaction().splits()) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account"); } d->m_storage->modifySchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched); } void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched); } MyMoneySchedule MyMoneyFile::schedule(const QString& id) const { return d->m_storage->schedule(id); } QList MyMoneyFile::scheduleList( const QString& accountId, const Schedule::Type type, const Schedule::Occurrence occurrence, const Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { d->checkStorage(); return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue); } QList MyMoneyFile::scheduleList( const QString& accountId) const { return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QList MyMoneyFile::scheduleList() const { return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QStringList MyMoneyFile::consistencyCheck() { QList list; QList::Iterator it_a; QList::Iterator it_sch; QList::Iterator it_p; QList::Iterator it_t; QList::Iterator it_r; QStringList accountRebuild; QMap interestAccounts; MyMoneyAccount parent; MyMoneyAccount child; MyMoneyAccount toplevel; QString parentId; QStringList rc; int problemCount = 0; int unfixedCount = 0; QString problemAccount; // check that we have a storage object d->checkTransaction(Q_FUNC_INFO); // get the current list of accounts accountList(list); // add the standard accounts list << MyMoneyFile::instance()->asset(); list << MyMoneyFile::instance()->liability(); list << MyMoneyFile::instance()->income(); list << MyMoneyFile::instance()->expense(); for (it_a = list.begin(); it_a != list.end(); ++it_a) { // no more checks for standard accounts if (isStandardAccount((*it_a).id())) { continue; } switch ((*it_a).accountGroup()) { case Account::Type::Asset: toplevel = asset(); break; case Account::Type::Liability: toplevel = liability(); break; case Account::Type::Expense: toplevel = expense(); break; case Account::Type::Income: toplevel = income(); break; case Account::Type::Equity: toplevel = equity(); break; default: qWarning("%s:%d This should never happen!", __FILE__ , __LINE__); break; } // check for loops in the hierarchy parentId = (*it_a).parentAccountId(); try { bool dropOut = false; while (!isStandardAccount(parentId) && !dropOut) { parent = account(parentId); if (parent.id() == (*it_a).id()) { // parent loops, so we need to re-parent to toplevel account // find parent account in our list problemCount++; QList::Iterator it_b; for (it_b = list.begin(); it_b != list.end(); ++it_b) { if ((*it_b).id() == parent.id()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name()); rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name()); (*it_a).setParentAccountId(toplevel.id()); if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); dropOut = true; break; } } } } parentId = parent.parentAccountId(); } } catch (const MyMoneyException &) { // if we don't know about a parent, we catch it later } // check that the parent exists parentId = (*it_a).parentAccountId(); try { parent = account(parentId); if ((*it_a).accountGroup() != parent.accountGroup()) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // the parent belongs to a different group, so we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name()); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account // and the one we removed this account from if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } else if (!parent.accountList().contains((*it_a).id())) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // parent exists, but does not have a reference to the account rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } } catch (const MyMoneyException &) { // apparently, the parent does not exist anymore. we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The parent with id %1 does not exist anymore.", parentId); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); } // now check that all the children exist and have the correct type foreach (const auto accountID, (*it_a).accountList()) { // check that the child exists try { child = account(accountID); if (child.parentAccountId() != (*it_a).id()) { throw MYMONEYEXCEPTION_CSTRING("Child account has a different parent"); } } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Child account with id %1 does not exist anymore.", accountID); rc << i18n(" The child account list will be reconstructed."); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); } } // see if it is a loan account. if so, remember the assigned interest account if ((*it_a).isLoan()) { MyMoneyAccountLoan loan(*it_a); if (!loan.interestAccountId().isEmpty()) { interestAccounts[loan.interestAccountId()] = true; } try { payee(loan.payee()); } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee()); rc << i18n(" The payee will be removed."); // remove the payee - the account will be modified in the engine later (*it_a).deletePair("payee"); } } // check if it is a category and set the date to 1900-01-01 if different if ((*it_a).isIncomeExpense()) { if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) { (*it_a).setOpeningDate(QDate(1900, 1, 1)); } } // check for clear text online password in the online settings if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext."); rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK."); rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version."); ++unfixedCount; } // if the account was modified, we need to update it in the engine if (!(d->m_storage->account((*it_a).id()) == (*it_a))) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data in engine."); return rc; } } } if (accountRebuild.count() != 0) { rc << i18n("* Reconstructing the child lists for"); } // clear the affected lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { rc << QString(" %1").arg((*it_a).name()); // clear the account list (*it_a).removeAccountIds(); } } // reconstruct the lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { QList::Iterator it; parentId = (*it_a).parentAccountId(); if (accountRebuild.contains(parentId)) { for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == parentId) { (*it).addAccountId((*it_a).id()); break; } } } } // update the engine objects for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name()); } } } // For some reason, files exist with invalid ids. This has been found in the payee id // so we fix them here QList pList = payeeList(); QMappayeeConversionMap; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if ((*it_p).id().length() > 7) { // found one of those with an invalid ids // create a new one and store it in the map. MyMoneyPayee payee = (*it_p); payee.clearId(); d->m_storage->addPayee(payee); payeeConversionMap[(*it_p).id()] = payee.id(); rc << i18n(" * Payee %1 recreated with fixed id", payee.name()); ++problemCount; } } // Fix the transactions MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); const auto tList = d->m_storage->transactionList(filter); // Generate the list of interest accounts for (const auto& transaction : tList) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) interestAccounts[split.accountId()] = true; } } QSet supportedAccountTypes; supportedAccountTypes << Account::Type::Checkings << Account::Type::Savings << Account::Type::Cash << Account::Type::CreditCard << Account::Type::Asset << Account::Type::Liability; QSet reportedUnsupportedAccounts; for (const auto& transaction : tList) { MyMoneyTransaction t = transaction; bool tChanged = false; QDate accountOpeningDate; QStringList accountList; const auto splits = t.splits(); foreach (const auto split, splits) { bool sChanged = false; MyMoneySplit s = split; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id()); ++problemCount; } try { const auto acc = this->account(s.accountId()); // compute the newest opening date of all accounts involved in the transaction // in case the newest opening date is newer than the transaction post date, do one // of the following: // // a) for category and stock accounts: update the opening date of the account // b) for account types where the user cannot modify the opening date through // the UI issue a warning (for each account only once) // c) others will be caught later if (!acc.isIncomeExpense() && !acc.isInvest()) { if (acc.openingDate() > t.postDate()) { if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) { accountOpeningDate = acc.openingDate(); } accountList << this->accountToCategory(acc.id()); if (!supportedAccountTypes.contains(acc.accountType()) && !reportedUnsupportedAccounts.contains(acc.id())) { rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.", this->accountToCategory(acc.id()), t.id()); reportedUnsupportedAccounts << acc.id(); ++unfixedCount; } } } else { if (acc.openingDate() > t.postDate()) { rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.", t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate)); rc << i18n(" Account opening date updated."); MyMoneyAccount newAcc = acc; newAcc.setOpeningDate(t.postDate()); this->modifyAccount(newAcc); ++problemCount; } } // make sure, that shares and value have the same number if they // represent the same currency. if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split of transaction '%1'.", t.id()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split of transaction '%1'.", t.id()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId()); ++unfixedCount; } // make sure the interest splits are marked correct as such if (interestAccounts.find(s.accountId()) != interestAccounts.end() && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); sChanged = true; rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id()); ++problemCount; } if (sChanged) { tChanged = true; t.modifySplit(s); } } // make sure that the transaction's post date is valid if (!t.postDate().isValid()) { tChanged = true; t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate()); rc << i18n(" * Transaction '%1' has an invalid post date.", t.id()); rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat)); ++problemCount; } // check if the transaction's post date is after the opening date // of all accounts involved in the transaction. In case it is not, // issue a warning with the details about the transaction incl. // the account names and dates involved if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) { QDate originalPostDate = t.postDate(); #if 0 // for now we do not activate the logic to move the post date to a later // point in time. This could cause some severe trouble if you have lots // of ancient data collected with older versions of KMyMoney that did not // enforce certain conditions like we do now. t.setPostDate(accountOpeningDate); tChanged = true; // copy the price information for investments to the new date QList::const_iterator it_t; foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = this->account(id); MyMoneySecurity sec = this->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); this->addPrice(price); break; } #endif rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat)); rc << i18n(" Referenced accounts: %1", accountList.join(",")); rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat)); ++unfixedCount; } if (tChanged) { d->m_storage->modifyTransaction(t); } } // Fix the schedules QList schList = scheduleList(); for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) { MyMoneySchedule sch = (*it_sch); MyMoneyTransaction t = sch.transaction(); auto tChanged = false; foreach (const auto split, t.splits()) { MyMoneySplit s = split; bool sChanged = false; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name()); ++problemCount; } if (!split.value().isZero() && split.shares().isZero()) { s.setShares(s.value()); sChanged = true; rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name()); rc << i18n(" Shares set to value."); ++problemCount; } // make sure, we don't have a bankid stored with a split in a schedule if (!split.bankID().isEmpty()) { s.setBankID(QString()); sChanged = true; rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name()); ++problemCount; } // make sure, that shares and value have the same number if they // represent the same currency. try { const auto acc = this->account(s.accountId()); if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId()); ++unfixedCount; } if (sChanged) { t.modifySplit(s); tChanged = true; } } if (tChanged) { sch.setTransaction(t); d->m_storage->modifySchedule(sch); } } // Fix the reports QList rList = reportList(); for (it_r = rList.begin(); it_r != rList.end(); ++it_r) { MyMoneyReport r = *it_r; QStringList payeeList; (*it_r).payees(payeeList); bool rChanged = false; for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) { if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; r.removeReference(*it_payee); r.addPayee(payeeConversionMap[*it_payee]); rChanged = true; } } if (rChanged) { d->m_storage->modifyReport(r); } } // erase old payee ids QMap::Iterator it_m; for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) { MyMoneyPayee payee = this->payee(it_m.key()); removePayee(payee); rc << i18n(" * Payee '%1' removed.", payee.id()); ++problemCount; } //look for accounts which have currencies other than the base currency but no price on the opening date //all accounts using base currency are excluded, since that's the base used for foreing currency calculation //thus it is considered as always present //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their //fake opening date since a forex rate is required for all multi-currency transactions //get all currencies in use QStringList currencyList; QList accountForeignCurrency; QList accList; accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { MyMoneyAccount account = *account_it; if (!account.isIncomeExpense() && !currencyList.contains(account.currencyId()) && account.currencyId() != baseCurrency().id() && !account.currencyId().isEmpty()) { //add the currency and the account-currency pair currencyList.append(account.currencyId()); accountForeignCurrency.append(account); } } MyMoneyPriceList pricesList = priceList(); QMap securityPriceDate; //get the first date of the price for each security MyMoneyPriceList::const_iterator prices_it; for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //only check the price if the currency is in use if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) { //check the security in the from field //if it is there, check if it is older QPair pricePair = qMakePair(firstPrice.from(), firstPrice.to()); securityPriceDate[pricePair] = firstPrice.date(); } } //compare the dates with the opening dates of the accounts using each currency QList::const_iterator accForeignList_it; bool firstInvProblem = true; for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) { //setup the price pair correctly QPair pricePair; //setup the reverse, which can also be used for rate conversion QPair reversePricePair; if ((*accForeignList_it).isInvest()) { //if it is a stock, we have to search for a price from its stock to the currency of the account QString securityId = (*accForeignList_it).currencyId(); QString tradingCurrencyId = security(securityId).tradingCurrency(); pricePair = qMakePair(securityId, tradingCurrencyId); reversePricePair = qMakePair(tradingCurrencyId, securityId); } else { //if it is a regular account we search for a price from the currency of the account to the base currency QString currency = (*accForeignList_it).currencyId(); QString baseCurrencyId = baseCurrency().id(); pricePair = qMakePair(currency, baseCurrencyId); reversePricePair = qMakePair(baseCurrencyId, currency); } //compare the first price with the opening date of the account if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate()) && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) { if (firstInvProblem) { firstInvProblem = false; rc << i18n("* Potential problem with investments/currencies"); } QDate openingDate = (*accForeignList_it).openingDate(); MyMoneySecurity secError = security((*accForeignList_it).currencyId()); if (!(*accForeignList_it).isInvest()) { rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the currency on or before the opening date."); } else { rc << i18n(" * The investment '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the investment on or before the opening date."); } ++unfixedCount; } } // Fix the budgets that somehow still reference invalid accounts QString problemBudget; QList bList = budgetList(); for (QList::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) { MyMoneyBudget b = *it_b; QList baccounts = b.getaccounts(); bool bChanged = false; for (QList::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) { try { account((*it_bacc).id()); } catch (const MyMoneyException &) { problemCount++; if (problemBudget != b.name()) { problemBudget = b.name(); rc << i18n("* Problem with budget '%1'", problemBudget); } rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id()); rc << i18n(" The account reference will be removed."); // remove the reference to the account b.removeReference((*it_bacc).id()); bChanged = true; } } if (bChanged) { d->m_storage->modifyBudget(b); } } // add more checks here if (problemCount == 0 && unfixedCount == 0) { rc << i18n("Finished: data is consistent."); } else { const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount); const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount); rc << QString(); rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining); } return rc; } QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent = base; QString categoryText; if (base.id() != expense().id() && base.id() != income().id()) throw MYMONEYEXCEPTION_CSTRING("Invalid base category"); QStringList subAccounts = name.split(AccountSeparator); QStringList::Iterator it; for (it = subAccounts.begin(); it != subAccounts.end(); ++it) { MyMoneyAccount categoryAccount; categoryAccount.setName(*it); categoryAccount.setAccountType(base.accountType()); if (it == subAccounts.begin()) categoryText += *it; else categoryText += (AccountSeparator + *it); // Only create the account if it doesn't exist try { QString categoryId = categoryToAccount(categoryText); if (categoryId.isEmpty()) addAccount(categoryAccount, parent); else { categoryAccount = account(categoryId); } } catch (const MyMoneyException &e) { qDebug("Unable to add account %s, %s, %s: %s", qPrintable(categoryAccount.name()), qPrintable(parent.name()), qPrintable(categoryText), e.what()); } parent = categoryAccount; } return categoryToAccount(name); } QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2) { QString accountId; MyMoneyAccount newAccount; bool found = true; if (!name.isEmpty()) { // The category might be constructed with an arbitraty depth (number of // colon delimited fields). We try to find a parent account within this // hierarchy by searching the following sequence: // // aaaa:bbbb:cccc:ddddd // // 1. search aaaa:bbbb:cccc:dddd, create nothing // 2. search aaaa:bbbb:cccc , create dddd // 3. search aaaa:bbbb , create cccc:dddd // 4. search aaaa , create bbbb:cccc:dddd // 5. don't search , create aaaa:bbbb:cccc:dddd newAccount.setName(name); QString accName; // part to be created (right side in above list) QString parent(name); // a possible parent part (left side in above list) do { accountId = categoryToAccount(parent); if (accountId.isEmpty()) { found = false; // prepare next step if (!accName.isEmpty()) accName.prepend(':'); accName.prepend(parent.section(':', -1)); newAccount.setName(accName); parent = parent.section(':', 0, -2); } else if (!accName.isEmpty()) { newAccount.setParentAccountId(accountId); } } while (!parent.isEmpty() && accountId.isEmpty()); // if we did not find the category, we create it if (!found) { MyMoneyAccount parentAccount; if (newAccount.parentAccountId().isEmpty()) { if (!value.isNegative() && value2.isNegative()) parentAccount = income(); else parentAccount = expense(); } else { parentAccount = account(newAccount.parentAccountId()); } newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense); MyMoneyAccount brokerage; // clear out the parent id, because createAccount() does not like that newAccount.setParentAccountId(QString()); createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney()); accountId = newAccount.id(); } } return accountId; } void MyMoneyFile::addSecurity(MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Add, security); } void MyMoneyFile::modifySecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifySecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security); } void MyMoneyFile::removeSecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // FIXME check that security is not referenced by other object d->m_storage->removeSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security); } MyMoneySecurity MyMoneyFile::security(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return baseCurrency(); return d->m_storage->security(id); } QList MyMoneyFile::securityList() const { d->checkStorage(); return d->m_storage->securityList(); } void MyMoneyFile::addCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency); } void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // force reload of base currency object if (currency.id() == d->m_baseCurrency.id()) d->m_baseCurrency.clearId(); d->m_storage->modifyCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency); } void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); if (currency.id() == d->m_baseCurrency.id()) throw MYMONEYEXCEPTION_CSTRING("Cannot delete base currency."); // FIXME check that security is not referenced by other object d->m_storage->removeCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency); } MyMoneySecurity MyMoneyFile::currency(const QString& id) const { if (id.isEmpty()) return baseCurrency(); try { const auto currency = d->m_storage->currency(id); if (currency.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id)); return currency; } catch (const MyMoneyException &) { const auto security = d->m_storage->security(id); if (security.id().isEmpty()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id)); } return security; } } QMap MyMoneyFile::ancientCurrencies() const { QMap ancientCurrencies; ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney"))); // http://en.wikipedia.org/wiki/Bulgarian_lev ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney"))); // Source: http://www.tf-portfoliosolutions.net/products/turkishlira.aspx ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney"))); // Source: http://en.wikipedia.org/wiki/Mozambican_metical ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); // Source https://en.wikipedia.org/wiki/Azerbaijani_manat ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Litas ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Belarusian_ruble ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); return ancientCurrencies; } QList MyMoneyFile::availableCurrencyList() const { QList currencyList; currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani"))); currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek"))); currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder"))); currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar"))); currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc"))); currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta"))); currencyList.append(MyMoneySecurity("AON", i18n("Angolan New Kwanza"))); currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$")); currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin"))); currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$")); currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m.")); currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$")); currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000)); currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka"))); currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$")); currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC")); currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br")); currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$")); currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$")); currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum"))); currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano"))); currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark"))); currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula"))); currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$")); currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$")); currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)"))); currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc"))); currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC"))); currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO"))); currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100)); currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel"))); currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$")); currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo"))); currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso"))); currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi"))); currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso"))); currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc"))); currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1))); currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna"))); currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso"))); currencyList.append(MyMoneySecurity("CUC", i18n("Cuban Convertible Peso"))); currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna"))); currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr")); currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc"))); currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso"))); currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$")); currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon"))); currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa"))); currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon"))); currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr"))); currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac))); currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$")); currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi"))); currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari"))); currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi"))); currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal"))); currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso"))); currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$")); currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde"))); currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira"))); currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$")); currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100)); currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona"))); currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20A8))); currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1)); currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1)); currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000)); currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA))); currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$")); currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1)); currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000)); currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge"))); currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling"))); currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000)); currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som"))); currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD))); currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats"))); currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti"))); currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$")); currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000)); currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca"))); currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar"))); currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500)); currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha"))); currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit"))); currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa"))); currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc"))); currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5)); currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee"))); currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$")); currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu"))); currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE))); currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham"))); currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT")); currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat"))); currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$")); currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee"))); currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$")); currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro"))); currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6))); currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr")); currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000)); currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee"))); currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa"))); currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina"))); currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani"))); currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol"))); currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1))); currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty"))); currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial"))); currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)"))); currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble"))); currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc"))); currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala"))); currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra"))); currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal"))); currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar"))); currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee"))); currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone"))); currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$")); currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling"))); currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand"))); currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee"))); currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar"))); currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder"))); currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni"))); currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona"))); currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr")); currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$")); currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni"))); currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling"))); currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F))); currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga"))); currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$")); currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000)); currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA))); currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat"))); currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$")); currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling"))); currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia"))); currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo"))); currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham"))); currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso"))); currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum"))); currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu"))); currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar"))); currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB))); currencyList.append(MyMoneySecurity("ZMK", i18n("Zambian Kwacha"))); currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$")); currencyList.append(ancientCurrencies().keys()); // sort the currencies ... qSort(currencyList.begin(), currencyList.end(), [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2) { return c1.name().compare(c2.name()) < 0; }); // ... and add a few precious metals at the ned currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000)); currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000)); currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000)); currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000)); return currencyList; } QList MyMoneyFile::currencyList() const { d->checkStorage(); return d->m_storage->currencyList(); } QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const { if (baseCurrency().id() == second) return first; return second; } MyMoneySecurity MyMoneyFile::baseCurrency() const { if (d->m_baseCurrency.id().isEmpty()) { QString id = QString(value("kmm-baseCurrency")); if (!id.isEmpty()) d->m_baseCurrency = currency(id); } return d->m_baseCurrency; } void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr) { // make sure the currency exists MyMoneySecurity c = currency(curr.id()); if (c.id() != d->m_baseCurrency.id()) { setValue("kmm-baseCurrency", curr.id()); // force reload of base currency cache d->m_baseCurrency = MyMoneySecurity(); } } void MyMoneyFile::addPrice(const MyMoneyPrice& price) { if (price.rate(QString()).isZero()) return; d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->addPrice(price); } void MyMoneyFile::removePrice(const MyMoneyPrice& price) { d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->removePrice(price); } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const { d->checkStorage(); QString to(toId); if (to.isEmpty()) to = value("kmm-baseCurrency"); // if some id is missing, we can return an empty price object if (fromId.isEmpty() || to.isEmpty()) return MyMoneyPrice(); // we don't search our tables if someone asks stupid stuff if (fromId == toId) { return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney"); } // if not asking for exact date, try to find the exact date match first, // either the requested price or its reciprocal value. If unsuccessful, it will move // on and look for prices of previous dates MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true); if (!rc.isValid()) { // not found, search 'to-from' rate and use reciprocal value rc = d->m_storage->price(to, fromId, date, true); // not found, search previous dates, if exact date is not needed if (!exactDate && !rc.isValid()) { // search 'from-to' and 'to-from', select the most recent one MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate); MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate); // check first whether both prices are valid if (fromPrice.isValid() && toPrice.isValid()) { if (fromPrice.date() >= toPrice.date()) { // if 'from-to' is newer or the same date, prefer that one rc = fromPrice; } else { // otherwise, use the reciprocal price rc = toPrice; } } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one rc = fromPrice; } else if (toPrice.isValid()) { rc = toPrice; } } } return rc; } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const { return price(fromId, toId, QDate::currentDate(), false); } MyMoneyPrice MyMoneyFile::price(const QString& fromId) const { return price(fromId, QString(), QDate::currentDate(), false); } MyMoneyPriceList MyMoneyFile::priceList() const { d->checkStorage(); return d->m_storage->priceList(); } bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const { const auto accounts = account(id).accountList(); for (const auto& acc : accounts) { if (account(acc).name().compare(name) == 0) return true; } return false; } QList MyMoneyFile::reportList() const { d->checkStorage(); return d->m_storage->reportList(); } void MyMoneyFile::addReport(MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addReport(report); } void MyMoneyFile::modifyReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyReport(report); } unsigned MyMoneyFile::countReports() const { d->checkStorage(); return d->m_storage->countReports(); } MyMoneyReport MyMoneyFile::report(const QString& id) const { d->checkStorage(); return d->m_storage->report(id); } void MyMoneyFile::removeReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeReport(report); } QList MyMoneyFile::budgetList() const { d->checkStorage(); return d->m_storage->budgetList(); } void MyMoneyFile::addBudget(MyMoneyBudget &budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addBudget(budget); } MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const { d->checkStorage(); return d->m_storage->budgetByName(name); } void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyBudget(budget); } unsigned MyMoneyFile::countBudgets() const { d->checkStorage(); return d->m_storage->countBudgets(); } MyMoneyBudget MyMoneyFile::budget(const QString& id) const { d->checkStorage(); return d->m_storage->budget(id); } void MyMoneyFile::removeBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeBudget(budget); } void MyMoneyFile::addOnlineJob(onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Add, job); } void MyMoneyFile::modifyOnlineJob(const onlineJob job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job); } onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const { d->checkStorage(); return d->m_storage->getOnlineJob(jobId); } QList MyMoneyFile::onlineJobList() const { d->checkStorage(); return d->m_storage->onlineJobList(); } /** @todo improve speed by passing count job to m_storage */ int MyMoneyFile::countOnlineJobs() const { return onlineJobList().count(); } /** * @brief Remove onlineJob * @param job onlineJob to remove */ void MyMoneyFile::removeOnlineJob(const onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache if (job.isLocked()) { return; } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job); d->m_storage->removeOnlineJob(job); } void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds) { foreach (QString jobId, onlineJobIds) { removeOnlineJob(getOnlineJob(jobId)); } } void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const { d->checkStorage(); list = d->m_storage->costCenterList(); } bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount) { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; MyMoneyAccount vatAcc = this->account(category.value("VatAccount").toLatin1()); const MyMoneySecurity& asec = security(account.currencyId()); const MyMoneySecurity& csec = security(category.currencyId()); const MyMoneySecurity& vsec = security(vatAcc.currencyId()); if (asec.id() != csec.id() || asec.id() != vsec.id()) { qDebug("Auto VAT assignment only works if all three accounts use the same currency."); return false; } MyMoneyMoney vatRate(vatAcc.value("VatRate")); MyMoneyMoney gv, nv; // gross value, net value int fract = account.fraction(); if (!vatRate.isZero()) { tax.setAccountId(vatAcc.id()); // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1()); if (category.value("VatAmount").toLower() != QString("net")) { // split value is the gross value gv = amount; nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit catSplit = transaction.splitByAccount(account.id(), false); catSplit.setShares(-nv); catSplit.setValue(catSplit.shares()); transaction.modifySplit(catSplit); } else { // split value is the net value nv = amount; gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit accSplit = transaction.splitByAccount(account.id()); accSplit.setValue(gv.convert(fract)); accSplit.setShares(accSplit.value()); transaction.modifySplit(accSplit); } tax.setValue(-(gv - nv).convert(fract)); tax.setShares(tax.value()); transaction.addSplit(tax); rc = true; } } catch (const MyMoneyException &) { } return rc; } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const { d->checkStorage(); return d->m_storage->isReferenced(obj, skipChecks); } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const { return isReferenced(obj, QBitArray((int)eStorage::Reference::Count)); } bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const { // by definition, an empty string or a non-numeric string is not used QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?")); if (no.isEmpty() || exp.indexIn(no) == -1) return false; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { MyMoneySplit split; // Test whether the transaction also includes a split into // this account split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty() && split.number() == no) return true; } catch (const MyMoneyException &) { } ++it_t; } return false; } QString MyMoneyFile::highestCheckNo(const QString& accId) const { unsigned64 lno = 0; unsigned64 cno; QString no; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty()) { // non-numerical values stored in number will return 0 in the next line cno = split.number().toULongLong(); if (cno > lno) { lno = cno; no = split.number(); } } } catch (const MyMoneyException &) { } ++it_t; } return no; } bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.setDateFilter(date.addDays(+1), QDate()); return !transactionList(filter).isEmpty(); } void MyMoneyFile::clearCache() { d->checkStorage(); d->m_balanceCache.clear(); } void MyMoneyFile::forceDataChanged() { emit dataChanged(); } bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const { - auto rc = false; + auto rc = true; if (t.splitCount() == 2) { foreach (const auto split, t.splits()) { auto acc = account(split.accountId()); if (acc.isIncomeExpense()) { - rc = true; + rc = false; break; } } } return rc; } bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const { auto ret = false; foreach (const auto split, t.splits()) { if (referencesClosedAccount(split)) { ret = true; break; } } return ret; } bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const { if (s.accountId().isEmpty()) return false; try { return account(s.accountId()).isClosed(); } catch (const MyMoneyException &) { } return false; } QString MyMoneyFile::storageId() { QString id = value("kmm-id"); if (id.isEmpty()) { MyMoneyFileTransaction ft; try { QUuid uid = QUuid::createUuid(); setValue("kmm-id", uid.toString()); ft.commit(); id = uid.toString(); } catch (const MyMoneyException &) { qDebug("Unable to setup UID for new storage object"); } } return id; } QString MyMoneyFile::openingBalancesPrefix() { return i18n("Opening Balances"); } bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const { // get current values auto acc = account(_acc.id()); // if there's no last transaction import data we are done if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) return false; // otherwise, we compare the balances MyMoneyMoney balance(acc.value("lastStatementBalance")); MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate)); return balance == accBalance; } int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.addState((int)state); return transactionList(filter).count(); } QMap > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const { QMap > result; MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); d->checkStorage(); QList list; accountList(list); for (const auto account : list) { result[account.id()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto transactions = d->m_storage->transactionList(filter); for (const auto& transaction : transactions) { for (const auto& split : transaction.splits()) { if (!result.contains(split.accountId())) { result[split.accountId()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto flag = split.reconcileFlag(); switch(flag) { case eMyMoney::Split::State::NotReconciled: case eMyMoney::Split::State::Cleared: case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: result[split.accountId()][(int)flag]++; break; default: break; } } } return result; } /** * Make sure that the splits value has the precision of the corresponding account */ void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const { auto transactionSecurity = security(t.commodity()); auto transactionFraction = transactionSecurity.smallestAccountFraction(); for (auto& split : t.splits()) { auto acc = account(split.accountId()); auto fraction = acc.fraction(); if(fraction == -1) { auto sec = security(acc.currencyId()); fraction = acc.fraction(sec); } split.setShares(static_cast(split.shares().convertDenominator(fraction).canonicalize())); split.setValue(static_cast(split.value().convertDenominator(transactionFraction).canonicalize())); } } class MyMoneyFileTransactionPrivate { Q_DISABLE_COPY(MyMoneyFileTransactionPrivate) public: MyMoneyFileTransactionPrivate() : m_isNested(MyMoneyFile::instance()->hasTransaction()), m_needRollback(!m_isNested) { } public: bool m_isNested; bool m_needRollback; }; MyMoneyFileTransaction::MyMoneyFileTransaction() : d_ptr(new MyMoneyFileTransactionPrivate) { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } MyMoneyFileTransaction::~MyMoneyFileTransaction() { try { rollback(); } catch (const MyMoneyException &e) { qDebug() << e.what(); } Q_D(MyMoneyFileTransaction); delete d; } void MyMoneyFileTransaction::restart() { rollback(); Q_D(MyMoneyFileTransaction); d->m_needRollback = !d->m_isNested; if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } void MyMoneyFileTransaction::commit() { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->commitTransaction(); d->m_needRollback = false; } void MyMoneyFileTransaction::rollback() { Q_D(MyMoneyFileTransaction); if (d->m_needRollback) MyMoneyFile::instance()->rollbackTransaction(); d->m_needRollback = false; } diff --git a/kmymoney/plugins/interfaces/CMakeLists.txt b/kmymoney/plugins/interfaces/CMakeLists.txt index 6dab49f44..6d4574b90 100644 --- a/kmymoney/plugins/interfaces/CMakeLists.txt +++ b/kmymoney/plugins/interfaces/CMakeLists.txt @@ -1,10 +1,10 @@ set (libinterfaces_a_SOURCES kmmappinterface.cpp kmmimportinterface.cpp kmmstatementinterface.cpp kmmviewinterface.cpp ) add_library(interfaces STATIC ${libinterfaces_a_SOURCES}) # TODO: fix dependencies -target_link_libraries(interfaces KF5::Service KF5::XmlGui Alkimia::alkimia Qt5::Xml) +target_link_libraries(interfaces KF5::Service KF5::XmlGui Alkimia::alkimia Qt5::Xml KF5::I18n) diff --git a/kmymoney/plugins/interfaces/kmmstatementinterface.cpp b/kmymoney/plugins/interfaces/kmmstatementinterface.cpp index b8ed8b5f2..297708c85 100644 --- a/kmymoney/plugins/interfaces/kmmstatementinterface.cpp +++ b/kmymoney/plugins/interfaces/kmmstatementinterface.cpp @@ -1,88 +1,109 @@ -/*************************************************************************** - kmmstatementinterface.cpp - ------------------- - begin : Wed Jan 5 2005 - copyright : (C) 2005 Thomas Baumgart - email : ipwizard@users.sourceforge.net - (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. * - * * - ***************************************************************************/ +/* + * Copyright 2005-2018 Thomas Baumgart + * Copyright 2017 Łukasz Wojniłowicz + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "kmmstatementinterface.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes +#include +#include + // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneystatementreader.h" KMyMoneyPlugin::KMMStatementInterface::KMMStatementInterface(QObject* parent, const char* name) : StatementInterface(parent, name) { } QStringList KMyMoneyPlugin::KMMStatementInterface::import(const MyMoneyStatement& s, bool silent) { qDebug("KMyMoneyPlugin::KMMStatementInterface::import start"); return MyMoneyStatementReader::importStatement(s, silent); } +void KMyMoneyPlugin::KMMStatementInterface::resetMessages() const +{ + MyMoneyStatementReader::clearResultMessages(); +} + +void KMyMoneyPlugin::KMMStatementInterface::showMessages(int statementCount) const +{ + const auto resultMessages = MyMoneyStatementReader::resultMessages(); + KMessageBox::informationList(nullptr, + i18np("One statement has been processed with the following results:", + "%1 statements have been processed with the following results:", + statementCount), + !resultMessages.isEmpty() ? + resultMessages : + QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) }, + i18n("Statement import statistics")); +} + MyMoneyAccount KMyMoneyPlugin::KMMStatementInterface::account(const QString& key, const QString& value) const { QList list; QList::const_iterator it_a; MyMoneyFile::instance()->accountList(list); QString accId; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { // search in the account's kvp container const auto& accountKvpValue = (*it_a).value(key); // search in the account's online settings kvp container const auto& onlineSettingsKvpValue = (*it_a).onlineBankingSettings().value(key); if (accountKvpValue.contains(value) || onlineSettingsKvpValue.contains(value)) { if(accId.isEmpty()) { accId = (*it_a).id(); } } if (accountKvpValue == value || onlineSettingsKvpValue == value) { accId = (*it_a).id(); break; } } // return the account found or an empty element return MyMoneyFile::instance()->account(accId); } void KMyMoneyPlugin::KMMStatementInterface::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const { MyMoneyFileTransaction ft; try { auto oAcc = MyMoneyFile::instance()->account(acc.id()); oAcc.setOnlineBankingSettings(kvps); MyMoneyFile::instance()->modifyAccount(oAcc); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to setup online parameters for account '%s'", qPrintable(acc.name())); // KMessageBox::detailedSorry(0, i18n("Unable to setup online parameters for account '%1'", acc.name()), QString::fromLatin1(e.what())); } } diff --git a/kmymoney/plugins/interfaces/kmmstatementinterface.h b/kmymoney/plugins/interfaces/kmmstatementinterface.h index 3e77e23e3..b0dc19358 100644 --- a/kmymoney/plugins/interfaces/kmmstatementinterface.h +++ b/kmymoney/plugins/interfaces/kmmstatementinterface.h @@ -1,72 +1,75 @@ -/*************************************************************************** - kmmstatementinterface.h - ------------------- - begin : Wed Jan 5 2005 - copyright : (C) 2005 Thomas Baumgart - email : ipwizard@users.sourceforge.net - (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. * - * * - ***************************************************************************/ +/* + * Copyright 2005-2018 Thomas Baumgart + * Copyright 2017 Łukasz Wojniłowicz + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef KMMSTATEMENTINTERFACE_H #define KMMSTATEMENTINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyAccount; class MyMoneyKeyValueContainer; #include "statementinterface.h" namespace KMyMoneyPlugin { /** * This class represents the implementation of the * StatementInterface. */ class KMMStatementInterface : public StatementInterface { Q_OBJECT public: explicit KMMStatementInterface(QObject* parent, const char* name = 0); ~KMMStatementInterface() {} + virtual void resetMessages() const final override; + virtual void showMessages(int statementCount) const final override; + /** * This method imports a MyMoneyStatement into the engine */ QStringList import(const MyMoneyStatement& s, bool silent = false) final override; /** * This method returns the account for a given @a key - @a value pair. * If the account is not found in the list of accounts, MyMoneyAccount() * is returned. The @a key - @a value pair can be in the account's kvp * container or the account's online settings kvp container. */ MyMoneyAccount account(const QString& key, const QString& value) const final override; /** * This method stores the online parameters in @a kvps used by the plugin * with the account @a acc. */ void setAccountOnlineParameters(const MyMoneyAccount&acc, const MyMoneyKeyValueContainer& kvps) const final override; }; } // namespace #endif diff --git a/kmymoney/plugins/kbanking/kbanking.cpp b/kmymoney/plugins/kbanking/kbanking.cpp index cbf889d95..f731fe6dc 100644 --- a/kmymoney/plugins/kbanking/kbanking.cpp +++ b/kmymoney/plugins/kbanking/kbanking.cpp @@ -1,1527 +1,1530 @@ -/*************************************************************************** - * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * - * Copyright 2009 Cristian Onet onet.cristian@gmail.com * - * Copyright 2010 Thomas Baumgart ipwizard@users.sourceforge.net * - * Copyright 2015 Christian David christian-david@web.de * - * * - * This program is free software; you can redistribute it and/or * - * modify it under the terms of the GNU General Public License as * - * published by the Free Software Foundation; either version 2 of * - * the License or (at your option) version 3 or any later version * - * accepted by the membership of KDE e.V. (or its successor approved * - * by the membership of KDE e.V.), which shall act as a proxy * - * defined in Section 14 of version 3 of the license. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see * - ***************************************************************************/ +/* + * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net + * Copyright 2009 Cristian Onet onet.cristian@gmail.com + * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org + * Copyright 2015 Christian David christian-david@web.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include #include "kbanking.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include //! @todo remove @c #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Library Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/onlinejob.h" #include "kbaccountsettings.h" #include "kbmapaccount.h" #include "mymoneyfile.h" #include "onlinejobadministration.h" #include "kmymoneyview.h" #include "kbpickstartdate.h" #include "mymoneyinstitution.h" #include "mymoneyexception.h" #include "gwenkdegui.h" #include "gwenhywfarqtoperators.h" #include "aqbankingkmmoperators.h" #include "mymoneystatement.h" #include "statementinterface.h" #include "viewinterface.h" #ifdef KMM_DEBUG // Added an option to open the chipTanDialog from the menu for debugging purposes #include "chiptandialog.h" #endif class KBanking::Private { public: Private() : passwordCacheTimer(nullptr), jobList(), fileId() { QString gwenProxy = QString::fromLocal8Bit(qgetenv("GWEN_PROXY")); if (gwenProxy.isEmpty()) { std::unique_ptr cfg = std::unique_ptr(new KConfig("kioslaverc")); QRegExp exp("(\\w+://)?([^/]{2}.+:\\d+)"); QString proxy; KConfigGroup grp = cfg->group("Proxy Settings"); int type = grp.readEntry("ProxyType", 0); switch (type) { case 0: // no proxy break; case 1: // manual specified proxy = grp.readEntry("httpsProxy"); qDebug("KDE https proxy setting is '%s'", qPrintable(proxy)); if (exp.exactMatch(proxy)) { proxy = exp.cap(2); qDebug("Setting GWEN_PROXY to '%s'", qPrintable(proxy)); if (!qputenv("GWEN_PROXY", qPrintable(proxy))) { qDebug("Unable to setup GWEN_PROXY"); } } break; default: // other currently not supported qDebug("KDE proxy setting of type %d not supported", type); break; } } } /** * KMyMoney asks for accounts over and over again which causes a lot of "Job not supported with this account" error messages. * This function filters messages with that string. */ static int gwenLogHook(GWEN_GUI* gui, const char* domain, GWEN_LOGGER_LEVEL level, const char* message) { Q_UNUSED(gui); Q_UNUSED(domain); Q_UNUSED(level); const char* messageToFilter = "Job not supported with this account"; if (strstr(message, messageToFilter) != 0) return 1; return 0; } QTimer *passwordCacheTimer; QMap jobList; QString fileId; }; KBanking::KBanking(QObject *parent, const QVariantList &args) : OnlinePluginExtended(parent, "kbanking") , d(new Private) , m_configAction(nullptr) , m_importAction(nullptr) , m_kbanking(nullptr) , m_accountSettings(nullptr) { Q_UNUSED(args) qDebug("Plugins: kbanking loaded"); } KBanking::~KBanking() { delete d; qDebug("Plugins: kbanking unloaded"); } void KBanking::plug() { m_kbanking = new KBankingExt(this, "KMyMoney"); d->passwordCacheTimer = new QTimer(this); d->passwordCacheTimer->setSingleShot(true); d->passwordCacheTimer->setInterval(60000); connect(d->passwordCacheTimer, &QTimer::timeout, this, &KBanking::slotClearPasswordCache); if (m_kbanking) { if (AB_Banking_HasConf4(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB4 config found."); if (AB_Banking_HasConf3(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB3 config found."); if (!AB_Banking_HasConf2(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: AqB2 config found - converting."); AB_Banking_ImportConf2(m_kbanking->getCInterface()); } } else { qDebug("KBankingPlugin: AqB3 config found - converting."); AB_Banking_ImportConf3(m_kbanking->getCInterface()); } } //! @todo when is gwenKdeGui deleted? gwenKdeGui *gui = new gwenKdeGui(); GWEN_Gui_SetGui(gui->getCInterface()); GWEN_Logger_SetLevel(0, GWEN_LoggerLevel_Warning); if (m_kbanking->init() == 0) { // Tell the host application to load my GUI component setComponentName("kbanking", "KBanking"); setXMLFile("kbanking.rc"); qDebug("Plugins: kbanking pluged"); // get certificate handling and dialog settings management AB_Gui_Extend(gui->getCInterface(), m_kbanking->getCInterface()); // create actions createActions(); // load protocol conversion list loadProtocolConversion(); GWEN_Logger_SetLevel(AQBANKING_LOGDOMAIN, GWEN_LoggerLevel_Warning); GWEN_Gui_SetLogHookFn(GWEN_Gui_GetGui(), &KBanking::Private::gwenLogHook); } else { qWarning("Could not initialize KBanking online banking interface"); delete m_kbanking; m_kbanking = 0; } } } void KBanking::unplug() { d->passwordCacheTimer->deleteLater(); if (m_kbanking) { m_kbanking->fini(); delete m_kbanking; qDebug("Plugins: kbanking unpluged"); } } void KBanking::loadProtocolConversion() { if (m_kbanking) { m_protocolConversionMap = { {"aqhbci", "HBCI"}, {"aqofxconnect", "OFX"}, {"aqyellownet", "YellowNet"}, {"aqgeldkarte", "Geldkarte"}, {"aqdtaus", "DTAUS"} }; } } void KBanking::protocols(QStringList& protocolList) const { if (m_kbanking) { std::list list = m_kbanking->getActiveProviders(); std::list::iterator it; for (it = list.begin(); it != list.end(); ++it) { // skip the dummy if (*it == "aqnone") continue; QMap::const_iterator it_m; it_m = m_protocolConversionMap.find((*it).c_str()); if (it_m != m_protocolConversionMap.end()) protocolList << (*it_m); else protocolList << (*it).c_str(); } } } QWidget* KBanking::accountConfigTab(const MyMoneyAccount& acc, QString& name) { const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); name = i18n("Online settings"); if (m_kbanking) { m_accountSettings = new KBAccountSettings(acc, 0); m_accountSettings->loadUi(kvp); return m_accountSettings; } QLabel* label = new QLabel(i18n("KBanking module not correctly initialized"), 0); label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); return label; } MyMoneyKeyValueContainer KBanking::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); kvp["provider"] = objectName().toLower(); if (m_accountSettings) { m_accountSettings->loadKvp(kvp); } return kvp; } void KBanking::createActions() { QAction *settings_aqbanking = actionCollection()->addAction("settings_aqbanking"); settings_aqbanking->setText(i18n("Configure Aq&Banking...")); connect(settings_aqbanking, &QAction::triggered, this, &KBanking::slotSettings); QAction *file_import_aqbanking = actionCollection()->addAction("file_import_aqbanking"); file_import_aqbanking->setText(i18n("AqBanking importer...")); connect(file_import_aqbanking, &QAction::triggered, this, &KBanking::slotImport); Q_CHECK_PTR(viewInterface()); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action("file_import_aqbanking"), &QAction::setEnabled); #ifdef KMM_DEBUG QAction *openChipTanDialog = actionCollection()->addAction("open_chiptan_dialog"); openChipTanDialog->setText("Open ChipTan Dialog"); connect(openChipTanDialog, &QAction::triggered, [&](){ auto dlg = new chipTanDialog(); dlg->setHhdCode("0F04871100030333555414312C32331D"); dlg->setInfoText("

Test Graphic for debugging

The encoded data is

Account Number: 335554
Amount: 1,23

"); connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater); connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater); dlg->show(); }); #endif } void KBanking::slotSettings() { if (m_kbanking) { GWEN_DIALOG* dlg = AB_SetupDialog_new(m_kbanking->getCInterface()); if (dlg == NULL) { DBG_ERROR(0, "Could not create setup dialog."); return; } if (GWEN_Gui_ExecDialog(dlg, 0) == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); return; } GWEN_Dialog_free(dlg); } } bool KBanking::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { bool rc = false; if (m_kbanking && !acc.id().isEmpty()) { m_kbanking->askMapAccount(acc); // at this point, the account should be mapped // so we search it and setup the account reference in the KMyMoney object AB_ACCOUNT* ab_acc; ab_acc = aqbAccount(acc); if (ab_acc) { MyMoneyAccount a(acc); setupAccountReference(a, ab_acc); settings = a.onlineBankingSettings(); rc = true; } } return rc; } AB_ACCOUNT* KBanking::aqbAccount(const MyMoneyAccount& acc) const { if (m_kbanking == 0) { return 0; } // certainly looking for an expense or income account does not make sense at this point // so we better get out right away if (acc.isIncomeExpense()) { return 0; } AB_ACCOUNT *ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), m_kbanking->mappingId(acc).toUtf8().data()); // if the account is not found, we temporarily scan for the 'old' mapping (the one w/o the file id) // in case we find it, we setup the new mapping in addition on the fly. if (!ab_acc && acc.isAssetLiability()) { ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), acc.id().toUtf8().data()); if (ab_acc) { qDebug("Found old mapping for '%s' but not new. Setup new mapping", qPrintable(acc.name())); m_kbanking->setAccountAlias(ab_acc, m_kbanking->mappingId(acc).toUtf8().constData()); // TODO at some point in time, we should remove the old mapping } } return ab_acc; } AB_ACCOUNT* KBanking::aqbAccount(const QString& accountId) const { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); return aqbAccount(account); } QString KBanking::stripLeadingZeroes(const QString& s) const { QString rc(s); QRegExp exp("^(0*)([^0].*)"); if (exp.exactMatch(s)) { rc = exp.cap(2); } return rc; } void KBanking::setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc) { MyMoneyKeyValueContainer kvp; if (ab_acc) { QString accountNumber = stripLeadingZeroes(AB_Account_GetAccountNumber(ab_acc)); QString routingNumber = stripLeadingZeroes(AB_Account_GetBankCode(ab_acc)); QString val = QString("%1-%2").arg(routingNumber, accountNumber); if (val != acc.onlineBankingSettings().value("kbanking-acc-ref")) { kvp.clear(); // make sure to keep our own previous settings const QMap& vals = acc.onlineBankingSettings().pairs(); QMap::const_iterator it_p; for (it_p = vals.begin(); it_p != vals.end(); ++it_p) { if (QString(it_p.key()).startsWith("kbanking-")) { kvp.setValue(it_p.key(), *it_p); } } kvp.setValue("kbanking-acc-ref", val); kvp.setValue("provider", objectName().toLower()); setAccountOnlineParameters(acc, kvp); } } else { // clear the connection setAccountOnlineParameters(acc, kvp); } } bool KBanking::accountIsMapped(const MyMoneyAccount& acc) { return aqbAccount(acc) != 0; } bool KBanking::updateAccount(const MyMoneyAccount& acc) { return updateAccount(acc, false); } bool KBanking::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { if (!m_kbanking) return false; bool rc = false; if (!acc.id().isEmpty()) { AB_JOB *job = 0; int rv; /* get AqBanking account */ AB_ACCOUNT *ba = aqbAccount(acc); // Update the connection between the KMyMoney account and the AqBanking equivalent. // If the account is not found anymore ba == 0 and the connection is removed. setupAccountReference(acc, ba); if (!ba) { KMessageBox::error(0, i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", acc.name()), i18n("Account Not Mapped")); } else { bool enqueJob = true; if (acc.onlineBankingSettings().value("kbanking-txn-download") != "no") { /* create getTransactions job */ job = AB_JobGetTransactions_new(ba); rv = AB_Job_CheckAvailability(job); if (rv) { DBG_ERROR(0, "Job \"GetTransactions\" is not available (%d)", rv); KMessageBox::error(0, i18n("" "The update job is not supported by the " "bank/account/backend.\n" ""), i18n("Job not Available")); AB_Job_free(job); job = 0; } if (job) { int days = AB_JobGetTransactions_GetMaxStoreDays(job); QDate qd; if (days > 0) { GWEN_TIME *ti1; GWEN_TIME *ti2; ti1 = GWEN_CurrentTime(); ti2 = GWEN_Time_fromSeconds(GWEN_Time_Seconds(ti1) - (60 * 60 * 24 * days)); GWEN_Time_free(ti1); ti1 = ti2; int year, month, day; if (GWEN_Time_GetBrokenDownDate(ti1, &day, &month, &year)) { DBG_ERROR(0, "Bad date"); qd = QDate(); } else qd = QDate(year, month + 1, day); GWEN_Time_free(ti1); } // get last statement request date from application account object // and start from a few days before if the date is valid QDate lastUpdate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate); if (lastUpdate.isValid()) lastUpdate = lastUpdate.addDays(-3); int dateOption = acc.onlineBankingSettings().value("kbanking-statementDate").toInt(); switch (dateOption) { case 0: // Ask user break; case 1: // No date qd = QDate(); break; case 2: // Last download qd = lastUpdate; break; case 3: // First possible // qd is already setup break; } // the pick start date option dialog is needed in // case the dateOption is 0 or the date option is > 1 // and the qd is invalid if (dateOption == 0 || (dateOption > 1 && !qd.isValid())) { QPointer psd = new KBPickStartDate(m_kbanking, qd, lastUpdate, acc.name(), lastUpdate.isValid() ? 2 : 3, 0, true); if (psd->exec() == QDialog::Accepted) { qd = psd->date(); } else { enqueJob = false; } delete psd; } if (enqueJob) { if (qd.isValid()) { GWEN_TIME *ti1; ti1 = GWEN_Time_new(qd.year(), qd.month() - 1, qd.day(), 0, 0, 0, 0); AB_JobGetTransactions_SetFromTime(job, ti1); GWEN_Time_free(ti1); } rv = m_kbanking->enqueueJob(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } } AB_Job_free(job); } } if (enqueJob) { /* create getBalance job */ job = AB_JobGetBalance_new(ba); rv = AB_Job_CheckAvailability(job); if (!rv) rv = m_kbanking->enqueueJob(job); else rv = 0; AB_Job_free(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } else { rc = true; emit queueChanged(); } } } } // make sure we have at least one job in the queue before sending it if (!moreAccounts && m_kbanking->getEnqueuedJobs().size() > 0) executeQueue(); return rc; } void KBanking::executeQueue() { if (m_kbanking && m_kbanking->getEnqueuedJobs().size() > 0) { AB_IMEXPORTER_CONTEXT *ctx; ctx = AB_ImExporterContext_new(); int rv = m_kbanking->executeQueue(ctx); if (!rv) { m_kbanking->importContext(ctx, 0); } else { DBG_ERROR(0, "Error: %d", rv); } AB_ImExporterContext_free(ctx); } } /** @todo improve error handling, e.g. by adding a .isValid to nationalTransfer * @todo use new onlineJob system */ void KBanking::sendOnlineJob(QList& jobs) { Q_CHECK_PTR(m_kbanking); m_onlineJobQueue.clear(); QList unhandledJobs; if (!jobs.isEmpty()) { foreach (onlineJob job, jobs) { if (sepaOnlineTransfer::name() == job.task()->taskName()) { onlineJobTyped typedJob(job); enqueTransaction(typedJob); job = typedJob; } else { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "KBanking", "Cannot handle this request")); unhandledJobs.append(job); } m_onlineJobQueue.insert(m_kbanking->mappingId(job), job); } executeQueue(); } jobs = m_onlineJobQueue.values() + unhandledJobs; m_onlineJobQueue.clear(); } QStringList KBanking::availableJobs(QString accountId) { try { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString id = MyMoneyFile::instance()->value("kmm-id"); if(id != d->fileId) { d->jobList.clear(); d->fileId = id; } } catch (const MyMoneyException &) { // Exception usually means account was not found return QStringList(); } if(d->jobList.contains(accountId)) { return d->jobList[accountId]; } QStringList list; AB_ACCOUNT* abAccount = aqbAccount(accountId); if (!abAccount) { return list; } // Check availableJobs // sepa transfer AB_JOB* abJob = AB_JobSepaTransfer_new(abAccount); if (AB_Job_CheckAvailability(abJob) == 0) list.append(sepaOnlineTransfer::name()); AB_Job_free(abJob); d->jobList[accountId] = list; return list; } /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbJobDeleter { public: static void cleanup(AB_JOB* job) { AB_Job_free(job); } }; /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbAccountDeleter { public: static void cleanup(AB_ACCOUNT* account) { AB_Account_free(account); } }; IonlineTaskSettings::ptr KBanking::settings(QString accountId, QString taskName) { AB_ACCOUNT* abAcc = aqbAccount(accountId); if (abAcc == 0) return IonlineTaskSettings::ptr(); if (sepaOnlineTransfer::name() == taskName) { // Get limits for sepaonlinetransfer QScopedPointer abJob(AB_JobSepaTransfer_new(abAcc)); if (AB_Job_CheckAvailability(abJob.data()) != 0) return IonlineTaskSettings::ptr(); const AB_TRANSACTION_LIMITS* limits = AB_Job_GetFieldLimits(abJob.data()); return AB_TransactionLimits_toSepaOnlineTaskSettings(limits).dynamicCast(); } return IonlineTaskSettings::ptr(); } bool KBanking::enqueTransaction(onlineJobTyped& job) { /* get AqBanking account */ const QString accId = job.constTask()->responsibleAccount(); AB_ACCOUNT *abAccount = aqbAccount(accId); if (!abAccount) { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Warning, "KBanking", i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", MyMoneyFile::instance()->account(accId).name()))); return false; } //setupAccountReference(acc, ba); // needed? AB_JOB *abJob = AB_JobSepaTransfer_new(abAccount); int rv = AB_Job_CheckAvailability(abJob); if (rv) { qDebug("AB_ERROR_OFFSET is %i", AB_ERROR_OFFSET); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "AqBanking", QString("Sepa credit transfers for account \"%1\" are not available, error code %2.").arg(MyMoneyFile::instance()->account(accId).name(), rv) ) ); return false; } AB_TRANSACTION *AbTransaction = AB_Transaction_new(); // Recipient payeeIdentifiers::ibanBic beneficiaryAcc = job.constTask()->beneficiaryTyped(); AB_Transaction_SetRemoteName(AbTransaction, GWEN_StringList_fromQString(beneficiaryAcc.ownerName())); AB_Transaction_SetRemoteIban(AbTransaction, beneficiaryAcc.electronicIban().toUtf8().constData()); AB_Transaction_SetRemoteBic(AbTransaction, beneficiaryAcc.fullStoredBic().toUtf8().constData()); // Origin Account AB_Transaction_SetLocalAccount(AbTransaction, abAccount); // Purpose QStringList qPurpose = job.constTask()->purpose().split('\n'); GWEN_STRINGLIST *purpose = GWEN_StringList_fromQStringList(qPurpose); AB_Transaction_SetPurpose(AbTransaction, purpose); GWEN_StringList_free(purpose); // Reference // AqBanking duplicates the string. This should be safe. AB_Transaction_SetEndToEndReference(AbTransaction, job.constTask()->endToEndReference().toUtf8().constData()); // Other Fields AB_Transaction_SetTextKey(AbTransaction, job.constTask()->textKey()); AB_Transaction_SetValue(AbTransaction, AB_Value_fromMyMoneyMoney(job.constTask()->value())); /** @todo LOW remove Debug info */ qDebug() << "SetTransaction: " << AB_Job_SetTransaction(abJob, AbTransaction); GWEN_DB_NODE *gwenNode = AB_Job_GetAppData(abJob); GWEN_DB_SetCharValue(gwenNode, GWEN_DB_FLAGS_DEFAULT, "kmmOnlineJobId", m_kbanking->mappingId(job).toLatin1().constData()); qDebug() << "Enqueue: " << m_kbanking->enqueueJob(abJob); //delete localAcc; return true; } void KBanking::startPasswordTimer() { if (d->passwordCacheTimer->isActive()) d->passwordCacheTimer->stop(); d->passwordCacheTimer->start(); } void KBanking::slotClearPasswordCache() { m_kbanking->clearPasswordCache(); } void KBanking::slotImport() { + m_statementCount = 0; + statementInterface()->resetMessages(); + if (!m_kbanking->interactiveImport()) qWarning("Error on import dialog"); + else + statementInterface()->showMessages(m_statementCount); } bool KBanking::importStatement(const MyMoneyStatement& s) { + m_statementCount++; return !statementInterface()->import(s).isEmpty(); } MyMoneyAccount KBanking::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void KBanking::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const { return statementInterface()->setAccountOnlineParameters(acc, kvps); } KBankingExt::KBankingExt(KBanking* parent, const char* appname, const char* fname) : AB_Banking(appname, fname) , m_parent(parent) , _jobQueue(0) { m_sepaKeywords = {QString::fromUtf8("SEPA-BASISLASTSCHRIFT"), QString::fromUtf8("SEPA-ÜBERWEISUNG")}; } int KBankingExt::init() { int rv = AB_Banking::init(); if (rv < 0) return rv; rv = onlineInit(); if (rv) { fprintf(stderr, "Error on online init (%d).\n", rv); AB_Banking::fini(); return rv; } _jobQueue = AB_Job_List2_new(); return 0; } int KBankingExt::fini() { if (_jobQueue) { AB_Job_List2_FreeAll(_jobQueue); _jobQueue = 0; } const int rv = onlineFini(); if (rv) { AB_Banking::fini(); return rv; } return AB_Banking::fini(); } int KBankingExt::executeQueue(AB_IMEXPORTER_CONTEXT *ctx) { m_parent->startPasswordTimer(); int rv = AB_Banking::executeJobs(_jobQueue, ctx); if (rv != 0) { qDebug() << "Sending queue by aqbanking got error no " << rv; } /** check result of each job */ AB_JOB_LIST2_ITERATOR* jobIter = AB_Job_List2_First(_jobQueue); if (jobIter) { AB_JOB* abJob = AB_Job_List2Iterator_Data(jobIter); while (abJob) { GWEN_DB_NODE* gwenNode = AB_Job_GetAppData(abJob); if (gwenNode == 0) { qWarning("Executed AB_Job without KMyMoney id"); abJob = AB_Job_List2Iterator_Next(jobIter); break; } QString jobIdent = QString::fromUtf8(GWEN_DB_GetCharValue(gwenNode, "kmmOnlineJobId", 0, "")); onlineJob job = m_parent->m_onlineJobQueue.value(jobIdent); if (job.isNull()) { // It should not be possiblie that this will happen (only if AqBanking fails heavily). //! @todo correct exception text qWarning("Executed a job which was not in queue. Please inform the KMyMoney developers."); abJob = AB_Job_List2Iterator_Next(jobIter); continue; } AB_JOB_STATUS abStatus = AB_Job_GetStatus(abJob); if (abStatus == AB_Job_StatusSent || abStatus == AB_Job_StatusPending || abStatus == AB_Job_StatusFinished || abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setJobSend(); if (abStatus == AB_Job_StatusFinished) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::acceptedByBank); else if (abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::sendingError); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KBanking", "Job was processed")); m_parent->m_onlineJobQueue.insert(jobIdent, job); abJob = AB_Job_List2Iterator_Next(jobIter); } AB_Job_List2Iterator_free(jobIter); } AB_JOB_LIST2 *oldQ = _jobQueue; _jobQueue = AB_Job_List2_new(); AB_Job_List2_FreeAll(oldQ); emit m_parent->queueChanged(); m_parent->startPasswordTimer(); return rv; } void KBankingExt::clearPasswordCache() { /* clear password DB */ GWEN_Gui_SetPasswordStatus(NULL, NULL, GWEN_Gui_PasswordStatus_Remove, 0); } std::list KBankingExt::getEnqueuedJobs() { AB_JOB_LIST2 *ll; std::list rl; ll = _jobQueue; if (ll && AB_Job_List2_GetSize(ll)) { AB_JOB *j; AB_JOB_LIST2_ITERATOR *it; it = AB_Job_List2_First(ll); assert(it); j = AB_Job_List2Iterator_Data(it); assert(j); while (j) { rl.push_back(j); j = AB_Job_List2Iterator_Next(it); } AB_Job_List2Iterator_free(it); } return rl; } int KBankingExt::enqueueJob(AB_JOB *j) { assert(_jobQueue); assert(j); AB_Job_Attach(j); AB_Job_List2_PushBack(_jobQueue, j); return 0; } int KBankingExt::dequeueJob(AB_JOB *j) { assert(_jobQueue); AB_Job_List2_Remove(_jobQueue, j); AB_Job_free(j); emit m_parent->queueChanged(); return 0; } void KBankingExt::transfer() { //m_parent->transfer(); } bool KBankingExt::askMapAccount(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); QString bankId; QString accountId; // extract some information about the bank. if we have a sortcode // (BLZ) we display it, otherwise the name is enough. try { const MyMoneyInstitution &bank = file->institution(acc.institutionId()); bankId = bank.name(); if (!bank.sortcode().isEmpty()) bankId = bank.sortcode(); } catch (const MyMoneyException &e) { // no bank assigned, we just leave the field emtpy } // extract account information. if we have an account number // we show it, otherwise the name will be displayed accountId = acc.number(); if (accountId.isEmpty()) accountId = acc.name(); // do the mapping. the return value of this method is either // true, when the user mapped the account or false, if he // decided to quit the dialog. So not really a great thing // to present some more information. KBMapAccount *w; w = new KBMapAccount(this, bankId.toUtf8().constData(), accountId.toUtf8().constData()); if (w->exec() == QDialog::Accepted) { AB_ACCOUNT *a; a = w->getAccount(); assert(a); DBG_NOTICE(0, "Mapping application account \"%s\" to " "online account \"%s/%s\"", qPrintable(acc.name()), AB_Account_GetBankCode(a), AB_Account_GetAccountNumber(a)); // TODO remove the following line once we don't need backward compatibility setAccountAlias(a, acc.id().toUtf8().constData()); qDebug("Setup mapping to '%s'", acc.id().toUtf8().constData()); setAccountAlias(a, mappingId(acc).toUtf8().constData()); qDebug("Setup mapping to '%s'", mappingId(acc).toUtf8().constData()); delete w; return true; } delete w; return false; } QString KBankingExt::mappingId(const MyMoneyObject& object) const { QString id = MyMoneyFile::instance()->storageId() + QLatin1Char('-') + object.id(); // AqBanking does not handle the enclosing parens, so we remove it id.remove('{'); id.remove('}'); return id; } bool KBankingExt::interactiveImport() { AB_IMEXPORTER_CONTEXT *ctx; GWEN_DIALOG *dlg; int rv; ctx = AB_ImExporterContext_new(); dlg = AB_ImporterDialog_new(getCInterface(), ctx, NULL); if (dlg == NULL) { DBG_ERROR(0, "Could not create importer dialog."); AB_ImExporterContext_free(ctx); return false; } rv = GWEN_Gui_ExecDialog(dlg, 0); if (rv == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } if (!importContext(ctx, 0)) { DBG_ERROR(0, "Error on importContext"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return true; } const AB_ACCOUNT_STATUS* KBankingExt::_getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai) { const AB_ACCOUNT_STATUS *ast; const AB_ACCOUNT_STATUS *best; best = 0; ast = AB_ImExporterAccountInfo_GetFirstAccountStatus(ai); while (ast) { if (!best) best = ast; else { const GWEN_TIME *tiBest; const GWEN_TIME *ti; tiBest = AB_AccountStatus_GetTime(best); ti = AB_AccountStatus_GetTime(ast); if (!tiBest) { best = ast; } else { if (ti) { double d; /* we have two times, compare them */ d = GWEN_Time_Diff(ti, tiBest); if (d > 0) /* newer */ best = ast; } } } ast = AB_ImExporterAccountInfo_GetNextAccountStatus(ai); } /* while */ return best; } void KBankingExt::_xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount& acc, const AB_TRANSACTION *t) { const GWEN_STRINGLIST *sl; QString s; QString memo; const char *p; const AB_VALUE *val; const GWEN_TIME *ti; const GWEN_TIME *startTime = 0; MyMoneyStatement::Transaction kt; unsigned long h; kt.m_fees = MyMoneyMoney(); // bank's transaction id p = AB_Transaction_GetFiId(t); if (p) kt.m_strBankID = QString("ID ") + QString::fromUtf8(p); // payee s.truncate(0); sl = AB_Transaction_GetRemoteName(t); if (sl) { GWEN_STRINGLISTENTRY *se; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); s += QString::fromUtf8(p); se = GWEN_StringListEntry_Next(se); } // while } kt.m_strPayee = s; // memo // The variable 's' contains the old method of extracting // the memo which added a linefeed after each part received // from AqBanking. The new variable 'memo' does not have // this inserted linefeed. We keep the variable 's' to // construct the hash-value to retrieve the reference s.truncate(0); sl = AB_Transaction_GetPurpose(t); if (sl) { GWEN_STRINGLISTENTRY *se; bool insertLineSep = false; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); if (insertLineSep) s += '\n'; insertLineSep = true; s += QString::fromUtf8(p).trimmed(); memo += QString::fromUtf8(p).trimmed(); se = GWEN_StringListEntry_Next(se); } // while // Sparda / Netbank hack: the software these banks use stores // parts of the payee name in the beginning of the purpose field // in case the payee name exceeds the 27 character limit. This is // the case, when one of the strings listed in m_sepaKeywords is part // of the purpose fields but does not start at the beginning. In this // case, the part leading up to the keyword is to be treated as the // tail of the payee. Also, a blank is inserted after the keyword. QSet::const_iterator itk; for (itk = m_sepaKeywords.constBegin(); itk != m_sepaKeywords.constEnd(); ++itk) { int idx = s.indexOf(*itk); if (idx >= 0) { if (idx > 0) { // re-add a possibly removed blank to name if (kt.m_strPayee.length() < 27) kt.m_strPayee += ' '; kt.m_strPayee += s.left(idx); s = s.mid(idx); } s = QString("%1 %2").arg(*itk).arg(s.mid((*itk).length())); // now do the same for 'memo' except for updating the payee idx = memo.indexOf(*itk); if (idx >= 0) { if (idx > 0) { memo = memo.mid(idx); } } memo = QString("%1 %2").arg(*itk).arg(memo.mid((*itk).length())); break; } } // in case we have some SEPA fields filled with information // we add them to the memo field p = AB_Transaction_GetEndToEndReference(t); if (p) { s += QString(", EREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("EREF: %1").arg(p)); } p = AB_Transaction_GetCustomerReference(t); if (p) { s += QString(", CREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CREF: %1").arg(p)); } p = AB_Transaction_GetMandateId(t); if (p) { s += QString(", MREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("MREF: %1").arg(p)); } p = AB_Transaction_GetCreditorSchemeId(t); if (p) { s += QString(", CRED: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CRED: %1").arg(p)); } p = AB_Transaction_GetOriginatorIdentifier(t); if (p) { s += QString(", DEBT: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("DEBT: %1").arg(p)); } } const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); // check if we need the version with or without linebreaks if (kvp.value("kbanking-memo-removelinebreaks").compare(QLatin1String("no"))) { kt.m_strMemo = memo; } else { kt.m_strMemo = s; } // calculate the hash code and start with the payee info // and append the memo field h = MyMoneyTransaction::hash(kt.m_strPayee.trimmed()); h = MyMoneyTransaction::hash(s, h); // see, if we need to extract the payee from the memo field QString rePayee = kvp.value("kbanking-payee-regexp"); if (!rePayee.isEmpty() && kt.m_strPayee.isEmpty()) { QString reMemo = kvp.value("kbanking-memo-regexp"); QStringList exceptions = kvp.value("kbanking-payee-exceptions").split(';', QString::SkipEmptyParts); bool needExtract = true; QStringList::const_iterator it_s; for (it_s = exceptions.constBegin(); needExtract && it_s != exceptions.constEnd(); ++it_s) { QRegExp exp(*it_s, Qt::CaseInsensitive); if (exp.indexIn(kt.m_strMemo) != -1) { needExtract = false; } } if (needExtract) { QRegExp expPayee(rePayee, Qt::CaseInsensitive); QRegExp expMemo(reMemo, Qt::CaseInsensitive); if (expPayee.indexIn(kt.m_strMemo) != -1) { kt.m_strPayee = expPayee.cap(1); if (expMemo.indexIn(kt.m_strMemo) != -1) { kt.m_strMemo = expMemo.cap(1); } } } } kt.m_strPayee = kt.m_strPayee.trimmed(); // date ti = AB_Transaction_GetDate(t); if (!ti) ti = AB_Transaction_GetValutaDate(t); if (ti) { int year, month, day; if (!startTime) startTime = ti; /*else { dead code if (GWEN_Time_Diff(ti, startTime) < 0) startTime = ti; }*/ if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) { kt.m_datePosted = QDate(year, month + 1, day); } } else { DBG_WARN(0, "No date for transaction"); } // value val = AB_Transaction_GetValue(t); if (val) { if (ks.m_strCurrency.isEmpty()) { p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } else { p = AB_Value_GetCurrency(val); if (p) s = p; if (ks.m_strCurrency.toLower() != s.toLower()) { // TODO: handle currency difference DBG_ERROR(0, "Mixed currencies currently not allowed"); } } kt.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val)); // The initial implementation of this feature was based on // a denominator of 100. Since the denominator might be // different nowadays, we make sure to use 100 for the // duplicate detection QString tmpVal = kt.m_amount.formatMoney(100, false); tmpVal.remove(QRegExp("[,\\.]")); tmpVal += QLatin1String("/100"); h = MyMoneyTransaction::hash(tmpVal, h); } else { DBG_WARN(0, "No value for transaction"); } if (startTime) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(startTime, &day, &month, &year)) { QDate d(year, month + 1, day); if (!ks.m_dateBegin.isValid()) ks.m_dateBegin = d; else if (d < ks.m_dateBegin) ks.m_dateBegin = d; if (!ks.m_dateEnd.isValid()) ks.m_dateEnd = d; else if (d > ks.m_dateEnd) ks.m_dateEnd = d; } } else { DBG_WARN(0, "No date in current transaction"); } // add information about remote account to memo in case we have something const char *remoteAcc = AB_Transaction_GetRemoteAccountNumber(t); const char *remoteBankCode = AB_Transaction_GetRemoteBankCode(t); if (remoteAcc && remoteBankCode) { kt.m_strMemo += QString("\n%1/%2").arg(remoteBankCode, remoteAcc); } // make hash value unique in case we don't have one already if (kt.m_strBankID.isEmpty()) { QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(kt.m_datePosted.toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = m_hashMap.constFind(hash); if (it == m_hashMap.constEnd()) { m_hashMap[hash] = true; break; } ++idx; } kt.m_strBankID = QString("%1-%2").arg(acc.id()).arg(hash); } // store transaction ks.m_listTransactions += kt; } bool KBankingExt::importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t /*flags*/) { const char *p; DBG_INFO(0, "Importing account..."); // account number MyMoneyStatement ks; p = AB_ImExporterAccountInfo_GetAccountNumber(ai); if (p) { ks.m_strAccountNumber = m_parent->stripLeadingZeroes(p); } p = AB_ImExporterAccountInfo_GetBankCode(ai); if (p) { ks.m_strRoutingNumber = m_parent->stripLeadingZeroes(p); } MyMoneyAccount kacc = m_parent->account("kbanking-acc-ref", QString("%1-%2").arg(ks.m_strRoutingNumber, ks.m_strAccountNumber)); ks.m_accountId = kacc.id(); // account name p = AB_ImExporterAccountInfo_GetAccountName(ai); if (p) ks.m_strAccountName = p; // account type switch (AB_ImExporterAccountInfo_GetType(ai)) { case AB_AccountType_Bank: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_CreditCard: ks.m_eType = eMyMoney::Statement::Type::CreditCard; break; case AB_AccountType_Checking: ks.m_eType = eMyMoney::Statement::Type::Checkings; break; case AB_AccountType_Savings: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_Investment: ks.m_eType = eMyMoney::Statement::Type::Investment; break; case AB_AccountType_Cash: default: ks.m_eType = eMyMoney::Statement::Type::None; } // account status const AB_ACCOUNT_STATUS* ast = _getAccountStatus(ai); if (ast) { const AB_BALANCE *bal; bal = AB_AccountStatus_GetBookedBalance(ast); if (!bal) bal = AB_AccountStatus_GetNotedBalance(ast); if (bal) { const AB_VALUE* val = AB_Balance_GetValue(bal); if (val) { DBG_INFO(0, "Importing balance"); ks.m_closingBalance = AB_Value_toMyMoneyMoney(val); p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } const GWEN_TIME* ti = AB_Balance_GetTime(bal); if (ti) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) ks.m_dateEnd = QDate(year, month + 1, day); } else { DBG_WARN(0, "No time for balance"); } } else { DBG_WARN(0, "No account balance"); } } else { DBG_WARN(0, "No account status"); } // clear hash map m_hashMap.clear(); // get all transactions const AB_TRANSACTION* t = AB_ImExporterAccountInfo_GetFirstTransaction(ai); while (t) { _xaToStatement(ks, kacc, t); t = AB_ImExporterAccountInfo_GetNextTransaction(ai); } // import them if (!m_parent->importStatement(ks)) { if (KMessageBox::warningYesNo(0, i18n("Error importing statement. Do you want to continue?"), i18n("Critical Error")) == KMessageBox::No) { DBG_ERROR(0, "User aborted"); return false; } } return true; } K_PLUGIN_FACTORY_WITH_JSON(KBankingFactory, "kbanking.json", registerPlugin();) #include "kbanking.moc" diff --git a/kmymoney/plugins/kbanking/kbanking.h b/kmymoney/plugins/kbanking/kbanking.h index bacbbe05a..45e4e3c1e 100644 --- a/kmymoney/plugins/kbanking/kbanking.h +++ b/kmymoney/plugins/kbanking/kbanking.h @@ -1,253 +1,253 @@ /*************************************************************************** * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * Copyright 2010 Thomas Baumgart ipwizard@users.sourceforge.net * * Copyright 2016 Christian David christian-david@web.de * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ #ifndef KBANKING_H #define KBANKING_H #ifdef HAVE_CONFIG_H #include #endif // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE & Library Includes class KAction; class QBanking; class KBankingExt; class KBAccountSettings; #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyplugin.h" #include "onlinepluginextended.h" #include "mymoneyaccount.h" #include "mymoneykeyvaluecontainer.h" #include "mymoney/onlinejobtyped.h" #include "onlinetasks/sepa/sepaonlinetransfer.h" #include "banking.hpp" /** * This class represents the KBanking plugin towards KMymoney. * All GUI related issues are handled in this object. */ class MyMoneyStatement; class KBanking : public KMyMoneyPlugin::OnlinePluginExtended { friend class KBankingExt; Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::OnlinePluginExtended KMyMoneyPlugin::OnlinePlugin) public: explicit KBanking(QObject *parent, const QVariantList &args); ~KBanking() override; bool importStatement(const MyMoneyStatement& s); MyMoneyAccount account(const QString& key, const QString& value) const; void setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const; void protocols(QStringList& protocolList) const override; QStringList availableJobs(QString accountId) override; IonlineTaskSettings::ptr settings(QString accountId, QString taskName) override; void sendOnlineJob(QList& jobs) override; void plug() override; void unplug() override; private: /** * creates the action objects available through the application menus */ void createActions(); /** * creates the context menu */ void createContextMenu(); /** * checks whether a given KMyMoney account with id @p id is * already mapped or not. * * @param acc reference to KMyMoney account object * @retval false account is not mapped to an AqBanking account * @retval true account is mapped to an AqBanking account */ bool accountIsMapped(const MyMoneyAccount& acc); /** * sets up the reference string consisting out of BLZ and account number * in the KMyMoney object so that we can find it later on when importing data. */ void setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc); /** * Returns the value of the parameter @a s with all leading 0's stripped. */ QString stripLeadingZeroes(const QString& s) const; /** * Prefills the protocol conversion list to allow mapping * of AqBanking internal names to external names */ void loadProtocolConversion(); /** * Creates an additional tab widget for the account edit dialog * to represent the necessary parameters for online banking * through AqBanking. */ QWidget* accountConfigTab(const MyMoneyAccount& acc, QString& name) override; /** * Stores the configuration data kept in the widgets created * in accountConfigTab() and returns them in a key value container * The current settings are accessible through the reference to * @a current. */ MyMoneyKeyValueContainer onlineBankingSettings(const MyMoneyKeyValueContainer& current) override; /** * Called by the application to map the KMyMoney account @a acc * to an AqBanking account. Calls KBanking to set up AqBanking mappings. * Returns the necessary settings for the plugin in @a settings and * @a true if the mapping was successful. */ bool mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) override; /** * This method translates a MyMoneyAccount to the corresponding AB_ACCOUNT object pointer. * If no mapped account can be detected, it returns 0. */ AB_ACCOUNT* aqbAccount(const MyMoneyAccount& acc) const; /** * This is a convenient method for aqbAccount if you have KMyMoney's account id only. */ AB_ACCOUNT* aqbAccount(const QString& accountId) const; /** * Called by the application framework to update the * KMyMoney account @a acc with data from the online source. * Store the jobs in the outbox in case @a moreAccounts is true */ bool updateAccount(const MyMoneyAccount& acc, bool moreAccounts) override; /** * Kept for backward compatibility. Use * updateAccount(const MyMoneyAccount& acc, bool moreAccounts) instead. * * @deprecated */ bool updateAccount(const MyMoneyAccount& acc) DEPRECATED; /** * Trigger the password cache timer */ void startPasswordTimer(); bool enqueTransaction(onlineJobTyped& job); protected Q_SLOTS: void slotSettings(); void slotImport(); void slotClearPasswordCache(); void executeQueue(); Q_SIGNALS: void queueChanged(); private: class Private; Private* const d; - KAction* m_configAction; - KAction* m_importAction; - KBankingExt* m_kbanking; - QMap m_protocolConversionMap; - KBAccountSettings* m_accountSettings; - + KAction* m_configAction; + KAction* m_importAction; + KBankingExt* m_kbanking; + QMap m_protocolConversionMap; + KBAccountSettings* m_accountSettings; + int m_statementCount; /** * @brief @ref onlineJob "onlineJobs" which are executed at the moment * Key is onlineJob->id(). This container is used during execution of jobs. */ QMap m_onlineJobQueue; }; /** * This class is the special implementation to glue the AB_Banking class * with the KMyMoneyPlugin structure. */ class KBankingExt : public AB_Banking { friend class KBanking; public: explicit KBankingExt(KBanking* parent, const char* appname, const char* fname = 0); virtual ~KBankingExt() {} int executeQueue(AB_IMEXPORTER_CONTEXT *ctx); int enqueueJob(AB_JOB *j); int dequeueJob(AB_JOB *j); std::list getEnqueuedJobs(); void transfer(); virtual bool interactiveImport(); protected: int init() final override; int fini() final override; bool askMapAccount(const MyMoneyAccount& acc); QString mappingId(const MyMoneyObject& object) const; bool importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t flags) final override; const AB_ACCOUNT_STATUS* _getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai); void _xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount&, const AB_TRANSACTION *t); void clearPasswordCache(); private: KBanking* m_parent; QMap m_hashMap; AB_JOB_LIST2 *_jobQueue; QSet m_sepaKeywords; }; #endif // KBANKING diff --git a/kmymoney/plugins/ofx/import/dialogs/konlinebankingsetupwizard.cpp b/kmymoney/plugins/ofx/import/dialogs/konlinebankingsetupwizard.cpp index 5457a46d5..802830580 100644 --- a/kmymoney/plugins/ofx/import/dialogs/konlinebankingsetupwizard.cpp +++ b/kmymoney/plugins/ofx/import/dialogs/konlinebankingsetupwizard.cpp @@ -1,565 +1,571 @@ /*************************************************************************** konlinebankingsetupwizard.cpp ------------------- begin : Sat Jan 7 2006 copyright : (C) 2006 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 "konlinebankingsetupwizard.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "../ofxpartner.h" #include "mymoneyofxconnector.h" using KWallet::Wallet; class KOnlineBankingSetupWizard::Private { public: Private() : m_prevPage(-1), m_wallet(0), m_walletIsOpen(false) {} QFile m_fpTrace; QTextStream m_trace; int m_prevPage; Wallet *m_wallet; bool m_walletIsOpen; }; KOnlineBankingSetupWizard::KOnlineBankingSetupWizard(QWidget *parent): QWizard(parent), d(new Private), m_fDone(false), m_fInit(false), m_appId(0) { setupUi(this); m_applicationEdit->hide(); m_headerVersionEdit->hide(); #ifndef LIBOFX_HAVE_CLIENTUID m_editClientUid->setEnabled(false); m_clientUidLabel->setEnabled(false); #endif m_appId = new OfxAppVersion(m_applicationCombo, m_applicationEdit, ""); m_headerVersion = new OfxHeaderVersion(m_headerVersionCombo, ""); // fill the list view with banks QProgressDialog* dlg = new QProgressDialog(this); dlg->setWindowTitle(i18n("Loading banklist")); dlg->setLabelText(i18n("Getting list of banks from http://www.ofxhome.com/\nThis may take some time depending on the available bandwidth.")); dlg->setModal(true); dlg->setCancelButton(0); // force to show immediately as the call to OfxPartner::BankNames() // does not call the processEvents() loop dlg->setMinimumDuration(0); QCoreApplication::processEvents(); //set password field according to KDE preferences m_editPassword->setPasswordMode(true); + // make sure to not exceed data fields + m_editUsername->setMaxLength(OFX_USERID_LENGTH-1); + m_editPassword->setMaxLength(OFX_USERPASS_LENGTH-1); + KListWidgetSearchLine* searchLine = new KListWidgetSearchLine(autoTab, m_listFi); vboxLayout1->insertWidget(0, searchLine); QTimer::singleShot(20, searchLine, SLOT(setFocus())); OfxPartner::setDirectory(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ""); m_listFi->addItems(OfxPartner::BankNames()); m_fInit = true; delete dlg; checkNextButton(); connect(this, SIGNAL(currentIdChanged(int)), this, SLOT(checkNextButton())); connect(this, SIGNAL(currentIdChanged(int)), this, SLOT(newPage(int))); connect(m_listFi, SIGNAL(itemSelectionChanged()), this, SLOT(checkNextButton())); connect(m_listAccount, SIGNAL(itemSelectionChanged()), this, SLOT(checkNextButton())); connect(m_selectionTab, SIGNAL(currentChanged(int)), this, SLOT(checkNextButton())); connect(m_fid, SIGNAL(userTextChanged(QString)), this, SLOT(checkNextButton())); connect(m_bankName, SIGNAL(userTextChanged(QString)), this, SLOT(checkNextButton())); connect(m_url, SIGNAL(textChanged(QString)), this, SLOT(checkNextButton())); connect(m_editUsername, SIGNAL(userTextChanged(QString)), this, SLOT(checkNextButton())); connect(m_editPassword, SIGNAL(userTextChanged(QString)), this, SLOT(checkNextButton())); connect(m_applicationEdit, SIGNAL(userTextChanged(QString)), this, SLOT(checkNextButton())); connect(m_applicationCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(applicationSelectionChanged())); // setup text on buttons setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next")); setButtonText(QWizard::BackButton, KStandardGuiItem::back().text()); // setup icons button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon()); button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon()); button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon()); button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon()); } KOnlineBankingSetupWizard::~KOnlineBankingSetupWizard() { delete m_appId; delete d; } void KOnlineBankingSetupWizard::applicationSelectionChanged() { m_applicationEdit->setVisible(m_appId->appId().endsWith(':')); checkNextButton(); } void KOnlineBankingSetupWizard::walletOpened(bool ok) { if (ok && (d->m_wallet->hasFolder(KWallet::Wallet::PasswordFolder()) || d->m_wallet->createFolder(KWallet::Wallet::PasswordFolder())) && d->m_wallet->setFolder(KWallet::Wallet::PasswordFolder())) { d->m_walletIsOpen = true; } else { qDebug("Wallet was not opened"); } m_storePassword->setEnabled(d->m_walletIsOpen); } void KOnlineBankingSetupWizard::checkNextButton() { bool enableButton = false; switch (currentId()) { case 0: if (m_selectionTab->currentIndex() == 0) { enableButton = (m_listFi->currentItem() != 0) && m_listFi->currentItem()->isSelected(); } else { enableButton = !(m_url->url().isEmpty() || m_bankName->text().isEmpty()); } break; case 1: enableButton = !(m_editUsername->text().isEmpty() || m_editPassword->text().isEmpty() || !m_appId->isValid()); break; case 2: enableButton = (m_listAccount->currentItem() != 0) && m_listAccount->currentItem()->isSelected(); break; } button(QWizard::NextButton)->setEnabled(enableButton); } void KOnlineBankingSetupWizard::newPage(int id) { QWidget* focus = 0; bool ok = true; if ((id - d->m_prevPage) == 1) { // one page forward? switch (d->m_prevPage) { case 0: ok = finishFiPage(); // open the KDE wallet if not already opened if (ok && !d->m_wallet) { d->m_wallet = Wallet::openWallet(Wallet::NetworkWallet(), winId(), Wallet::Asynchronous); connect(d->m_wallet, SIGNAL(walletOpened(bool)), SLOT(walletOpened(bool))); } focus = m_editUsername; break; case 1: ok = finishLoginPage(); focus = m_listAccount; break; case 2: m_fDone = ok = finishAccountPage(); break; } if (ok) { if (focus) { focus->setFocus(); } } else { // force to go back to prev page back(); } } else { // going backwards, we're never done m_fDone = false; } button(QWizard::FinishButton)->setEnabled(m_fDone); // hide cancel and back button on last page button(QWizard::CancelButton)->setVisible(!m_fDone); button(QWizard::BackButton)->setVisible(!m_fDone); if (ok) d->m_prevPage = id; } bool KOnlineBankingSetupWizard::finishFiPage() { bool result = false; m_bankInfo.clear(); OfxFiServiceInfo info; if (m_selectionTab->currentIndex() == 0) { // Get the fipids for the selected bank QListWidgetItem* item = m_listFi->currentItem(); if (item && item->isSelected()) { QString bank = item->text(); m_textDetails->clear(); m_textDetails->append(QString("

Details for %1:

").arg(bank)); QStringList fipids = OfxPartner::FipidForBank(bank); QStringList::const_iterator it_fipid = fipids.constBegin(); while (it_fipid != fipids.constEnd()) { // For each fipid, get the connection details info = OfxPartner::ServiceInfo(*it_fipid); // Print them to the text browser QString message = QString("

Fipid: %1
").arg(*it_fipid); // If the bank supports retrieving statements if (info.accountlist) { m_bankInfo.push_back(info); message += QString("URL: %1
Org: %2
Fid: %3
").arg(info.url, info.org, info.fid); if (info.statements) message += i18n("Supports online statements
"); if (info.investments) message += i18n("Supports investments
"); if (info.billpay) message += i18n("Supports bill payment (but not supported by KMyMoney yet)
"); } else { message += i18n("Does not support online banking"); } message += "

"; m_textDetails->append(message); ++it_fipid; } result = true; } else // error! No current item KMessageBox::sorry(this, i18n("Please choose a bank.")); } else { // manual entry of values if (m_fid->text().isEmpty() || m_url->url().isEmpty() || m_bankName->text().isEmpty()) { KMessageBox::sorry(this, i18n("Please fill all fields with values.")); } m_textDetails->clear(); m_textDetails->append(i18n("

Details for %1:

", m_bankName->text())); memset(&info, 0, sizeof(OfxFiServiceInfo)); strncpy(info.fid, m_fid->text().toLatin1(), OFX_FID_LENGTH - 1); strncpy(info.org, m_bankName->text().toLatin1(), OFX_ORG_LENGTH - 1); strncpy(info.url, m_url->url().url().toLatin1(), OFX_URL_LENGTH - 1); info.accountlist = 1; info.statements = 1; info.billpay = 1; info.investments = 1; m_bankInfo.push_back(info); QString message; message += QString("

URL: %1
Org: %2
Fid: %3
").arg(info.url, info.org, info.fid); if (info.statements) message += i18n("Supports online statements
"); if (info.investments) message += i18n("Supports investments
"); if (info.billpay) message += i18n("Supports bill payment (but not supported by KMyMoney yet)
"); message += "

"; m_textDetails->append(message); result = true; } // make sure to display the beginning of the collected information m_textDetails->moveCursor(QTextCursor::Start); return result; } bool KOnlineBankingSetupWizard::finishLoginPage() { bool result = true; QString username = m_editUsername->text(); QString password = m_editPassword->text(); QString clientUid = m_editClientUid->text(); m_listAccount->clear(); // Process an account request for each fipid m_it_info = m_bankInfo.constBegin(); while (m_it_info != m_bankInfo.constEnd()) { OfxFiLogin fi; memset(&fi, 0, sizeof(OfxFiLogin)); - strncpy(fi.fid, (*m_it_info).fid, OFX_FID_LENGTH - 1); - strncpy(fi.org, (*m_it_info).org, OFX_ORG_LENGTH - 1); - strncpy(fi.userid, username.toLatin1(), OFX_USERID_LENGTH - 1); + Q_ASSERT(sizeof(fi.fid) == sizeof((*m_it_info).fid)); + Q_ASSERT(sizeof(fi.org) == sizeof((*m_it_info).org)); + memcpy(fi.fid, (*m_it_info).fid, OFX_FID_LENGTH - 1); + memcpy(fi.org, (*m_it_info).org, OFX_ORG_LENGTH - 1); + strncpy(fi.userid, username.toLatin1().left(OFX_USERID_LENGTH - 1), OFX_USERID_LENGTH - 1); strncpy(fi.userpass, password.toLatin1(), OFX_USERPASS_LENGTH - 1); #ifdef LIBOFX_HAVE_CLIENTUID strncpy(fi.clientuid, clientUid.toLatin1(), OFX_CLIENTUID_LENGTH - 1); #endif // pretend we're Quicken 2008 // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-intuit-products/ // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-microsoft-money/ QString appId = m_appId->appId(); QRegExp exp("(.*):(.*)"); if (exp.indexIn(appId) != -1) { strncpy(fi.appid, exp.cap(1).toLatin1(), OFX_APPID_LENGTH - 1); if (exp.cap(2).isEmpty()) { strncpy(fi.appver, m_applicationEdit->text().toLatin1(), OFX_APPVER_LENGTH - 1); } else { strncpy(fi.appver, exp.cap(2).toLatin1(), OFX_APPVER_LENGTH - 1); } } else { strncpy(fi.appid, "QWIN", OFX_APPID_LENGTH - 1); strncpy(fi.appver, "1700", OFX_APPVER_LENGTH - 1); } QString hver = m_headerVersion->headerVersion(); strncpy(fi.header_version, hver.toLatin1(), OFX_HEADERVERSION_LENGTH - 1); QUrl filename(QString("file://%1response.ofx").arg(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + "")); QByteArray req(libofx_request_accountinfo(&fi)); // because the event loop is running while the request is performed disable the back button // (this function is not reentrant so the application might crash when back/next are used) QAbstractButton *backButton = button(QWizard::BackButton); bool backButtonState = backButton->isEnabled(); backButton->setEnabled(false); OfxHttpRequest(QString("POST"), QUrl((*m_it_info).url), req, QMap(), filename, false); backButton->setEnabled(backButtonState); LibofxContextPtr ctx = libofx_get_new_context(); Q_CHECK_PTR(ctx); ofx_set_account_cb(ctx, ofxAccountCallback, this); ofx_set_status_cb(ctx, ofxStatusCallback, this); // Add resulting accounts to the account list libofx_proc_file(ctx, filename.path().toLatin1(), AUTODETECT); libofx_free_context(ctx); ++m_it_info; } if (! m_listAccount->topLevelItem(0)) { KMessageBox::sorry(this, i18n("No suitable accounts were found at this bank.")); result = false; } else { m_listAccount->resizeColumnToContents(0); m_listAccount->resizeColumnToContents(1); m_listAccount->resizeColumnToContents(2); m_listAccount->resizeColumnToContents(3); } return result; } bool KOnlineBankingSetupWizard::finishAccountPage() { bool result = true; if (! m_listAccount->currentItem()) { KMessageBox::sorry(this, i18n("Please choose an account")); result = false; } return result; } int KOnlineBankingSetupWizard::ofxAccountCallback(struct OfxAccountData data, void * pv) { KOnlineBankingSetupWizard* pthis = reinterpret_cast(pv); // Put the account info in the view MyMoneyKeyValueContainer kvps; if (data.account_type_valid) { QString type; switch (data.account_type) { case OfxAccountData::OFX_CHECKING: /**< A standard checking account */ type = "CHECKING"; break; case OfxAccountData::OFX_SAVINGS: /**< A standard savings account */ type = "SAVINGS"; break; case OfxAccountData::OFX_MONEYMRKT: /**< A money market account */ type = "MONEY MARKET"; break; case OfxAccountData::OFX_CREDITLINE: /**< A line of credit */ type = "CREDIT LINE"; break; case OfxAccountData::OFX_CMA: /**< Cash Management Account */ type = "CMA"; break; case OfxAccountData::OFX_CREDITCARD: /**< A credit card account */ type = "CREDIT CARD"; break; case OfxAccountData::OFX_INVESTMENT: /**< An investment account */ type = "INVESTMENT"; break; default: break; } kvps.setValue("type", type); } if (data.bank_id_valid) kvps.setValue("bankid", data.bank_id); if (data.broker_id_valid) kvps.setValue("bankid", data.broker_id); if (data.branch_id_valid) kvps.setValue("branchid", data.branch_id); if (data.account_number_valid) kvps.setValue("accountid", data.account_number); if (data.account_id_valid) kvps.setValue("uniqueId", data.account_id); kvps.setValue("username", pthis->m_editUsername->text()); kvps.setValue("password", pthis->m_editPassword->text()); #ifdef LIBOFX_HAVE_CLIENTUID kvps.setValue("clientUid", pthis->m_editClientUid->text()); #endif kvps.setValue("url", (*(pthis->m_it_info)).url); kvps.setValue("fid", (*(pthis->m_it_info)).fid); kvps.setValue("org", (*(pthis->m_it_info)).org); kvps.setValue("fipid", ""); QListWidgetItem* item = pthis->m_listFi->currentItem(); if (item) kvps.setValue("bankname", item->text()); // I removed the bankid here, because for some users it // was not possible to setup the automatic account matching // because the bankid was left empty here as well during // the statement download. In case we don't have it, we // simply use it blank. (ipwizard 2009-06-21) if (/* !kvps.value("bankid").isEmpty() && */ !kvps.value("uniqueId").isEmpty()) { kvps.setValue("kmmofx-acc-ref", QString("%1-%2").arg(kvps.value("bankid"), kvps.value("uniqueId"))); } else { qDebug("Cannot setup kmmofx-acc-ref for '%s'", qPrintable(kvps.value("bankname"))); } kvps.setValue("protocol", "OFX"); new ListViewItem(pthis->m_listAccount, kvps); return 0; } int KOnlineBankingSetupWizard::ofxStatusCallback(struct OfxStatusData data, void * pv) { KOnlineBankingSetupWizard* pthis = reinterpret_cast(pv); QString message; if (data.code_valid) { message += QString("#%1 %2: \"%3\"\n").arg(data.code).arg(data.name, data.description); } if (data.server_message_valid) { message += i18n("Server message: %1\n", data.server_message); } if (data.severity_valid) { switch (data.severity) { case OfxStatusData::INFO : break; case OfxStatusData::WARN : KMessageBox::detailedError(pthis, i18n("Your bank returned warnings when signing on"), i18nc("Warning 'message'", "WARNING %1", message)); break; case OfxStatusData::ERROR : KMessageBox::detailedError(pthis, i18n("Error signing onto your bank"), i18n("ERROR %1", message)); break; default: break; } } return 0; } bool KOnlineBankingSetupWizard::chosenSettings(MyMoneyKeyValueContainer& settings) { bool result = false;; if (m_fDone) { QTreeWidgetItem* qitem = m_listAccount->currentItem(); ListViewItem* item = dynamic_cast(qitem); if (item && item->isSelected()) { settings = *item; settings.deletePair("appId"); settings.deletePair("kmmofx-headerVersion"); QString appId = m_appId->appId(); if (!appId.isEmpty()) { if (appId.endsWith(':')) { appId += m_applicationEdit->text(); } settings.setValue("appId", appId); } QString hVer = m_headerVersion->headerVersion(); if (!hVer.isEmpty()) settings.setValue("kmmofx-headerVersion", hVer); if (m_storePassword->isChecked()) { if (d->m_walletIsOpen) { QString key = OFX_PASSWORD_KEY(settings.value("url"), settings.value("uniqueId")); d->m_wallet->writePassword(key, settings.value("password")); settings.deletePair("password"); } } else { settings.deletePair("password"); } result = true; } } return result; } KOnlineBankingSetupWizard::ListViewItem::ListViewItem(QTreeWidget* parent, const MyMoneyKeyValueContainer& kvps): MyMoneyKeyValueContainer(kvps), QTreeWidgetItem(parent) { setText(0, value("accountid")); setText(1, value("type")); setText(2, value("bankid")); setText(3, value("branchid")); } diff --git a/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp b/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp index e690b4360..f7977e694 100644 --- a/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp +++ b/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp @@ -1,773 +1,773 @@ /*************************************************************************** mymoneyofxconnector.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 "mymoneyofxconnector.h" // ---------------------------------------------------------------------------- // System Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneyenums.h" using KWallet::Wallet; OfxHeaderVersion::OfxHeaderVersion(KComboBox* combo, const QString& headerVersion) : m_combo(combo) { combo->clear(); combo->addItem("102"); combo->addItem("103"); if (!headerVersion.isEmpty()) { combo->setCurrentItem(headerVersion); } else { combo->setCurrentItem("102"); } } QString OfxHeaderVersion::headerVersion() const { return m_combo->currentText(); } OfxAppVersion::OfxAppVersion(KComboBox* combo, KLineEdit* versionEdit, const QString& appId) : m_combo(combo), m_versionEdit(versionEdit) { // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-intuit-products/ // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-microsoft-money/ // Quicken m_appMap[i18n("Quicken Windows 2003")] = "QWIN:1200"; m_appMap[i18n("Quicken Windows 2004")] = "QWIN:1300"; m_appMap[i18n("Quicken Windows 2005")] = "QWIN:1400"; m_appMap[i18n("Quicken Windows 2006")] = "QWIN:1500"; m_appMap[i18n("Quicken Windows 2007")] = "QWIN:1600"; m_appMap[i18n("Quicken Windows 2008")] = "QWIN:1700"; // the following three added as found on // https://microsoftmoneyoffline.wordpress.com/appid-appver/ on 2013-02-28 m_appMap[i18n("Quicken Windows 2010")] = "QWIN:1800"; m_appMap[i18n("Quicken Windows 2011")] = "QWIN:1900"; m_appMap[i18n("Quicken Windows 2012")] = "QWIN:2100"; m_appMap[i18n("Quicken Windows 2013")] = "QWIN:2200"; m_appMap[i18n("Quicken Windows 2014")] = "QWIN:2300"; // following two added as found in previous URL on 2017-10-01 m_appMap[i18n("Quicken Windows 2015")] = "QWIN:2400"; m_appMap[i18n("Quicken Windows 2016")] = "QWIN:2500"; m_appMap[i18n("Quicken Windows (Expert)")] = "QWIN:"; // MS-Money m_appMap[i18n("MS-Money 2003")] = "Money:1100"; m_appMap[i18n("MS-Money 2004")] = "Money:1200"; m_appMap[i18n("MS-Money 2005")] = "Money:1400"; m_appMap[i18n("MS-Money 2006")] = "Money:1500"; m_appMap[i18n("MS-Money 2007")] = "Money:1600"; m_appMap[i18n("MS-Money Plus")] = "Money:1700"; m_appMap[i18n("MS-Money (Expert)")] = "Money:"; // KMyMoney m_appMap["KMyMoney"] = "KMyMoney:1000"; combo->clear(); combo->addItems(m_appMap.keys()); if (versionEdit) versionEdit->hide(); QMap::const_iterator it_a; // check for an exact match for (it_a = m_appMap.constBegin(); it_a != m_appMap.constEnd(); ++it_a) { if (*it_a == appId) break; } // not found, check if we have a manual version of this product QRegExp appExp("(\\w+:)(\\d+)"); if (it_a == m_appMap.constEnd()) { if (appExp.exactMatch(appId)) { for (it_a = m_appMap.constBegin(); it_a != m_appMap.constEnd(); ++it_a) { if (*it_a == appExp.cap(1)) break; } } } // if we still haven't found it, we use a default as last resort if (it_a != m_appMap.constEnd()) { combo->setCurrentItem(it_a.key()); if ((*it_a).endsWith(':')) { if (versionEdit) { versionEdit->show(); versionEdit->setText(appExp.cap(2)); } else { combo->setCurrentItem(i18n("Quicken Windows 2008")); } } } else { combo->setCurrentItem(i18n("Quicken Windows 2008")); } } const QString OfxAppVersion::appId() const { static QString defaultAppId("QWIN:1700"); QString app = m_combo->currentText(); if (m_appMap[app] != defaultAppId) { if (m_appMap[app].endsWith(':')) { if (m_versionEdit) { return m_appMap[app] + m_versionEdit->text(); } else { return QString(); } } return m_appMap[app]; } return QString(); } bool OfxAppVersion::isValid() const { QRegExp exp(".+:\\d+"); QString app = m_combo->currentText(); if (m_appMap[app].endsWith(':')) { if (m_versionEdit) { app = m_appMap[app] + m_versionEdit->text(); } else { app.clear(); } } else { app = m_appMap[app]; } return exp.exactMatch(app); } MyMoneyOfxConnector::MyMoneyOfxConnector(const MyMoneyAccount& _account): m_account(_account) { m_fiSettings = m_account.onlineBankingSettings(); } QString MyMoneyOfxConnector::iban() const { return m_fiSettings.value("bankid"); } QString MyMoneyOfxConnector::fiorg() const { return m_fiSettings.value("org"); } QString MyMoneyOfxConnector::fiid() const { return m_fiSettings.value("fid"); } QString MyMoneyOfxConnector::clientUid() const { return m_fiSettings.value("clientUid"); } QString MyMoneyOfxConnector::username() const { return m_fiSettings.value("username"); } QString MyMoneyOfxConnector::password() const { // if we don't find a password in the wallet, we use the old method // and retrieve it from the settings stored in the KMyMoney data storage. // in case we don't have a password on file, we ask the user QString key = OFX_PASSWORD_KEY(m_fiSettings.value("url"), m_fiSettings.value("uniqueId")); QString pwd = m_fiSettings.value("password"); // now check for the wallet Wallet *wallet = openSynchronousWallet(); if (wallet && !Wallet::keyDoesNotExist(Wallet::NetworkWallet(), Wallet::PasswordFolder(), key)) { wallet->setFolder(Wallet::PasswordFolder()); wallet->readPassword(key, pwd); } if (pwd.isEmpty()) { QPointer dlg = new KPasswordDialog(0); dlg->setPrompt(i18n("Enter your password for account %1", m_account.name())); if (dlg->exec()) pwd = dlg->password(); delete dlg; } return pwd; } QString MyMoneyOfxConnector::accountnum() const { return m_fiSettings.value("accountid"); } QString MyMoneyOfxConnector::url() const { return m_fiSettings.value("url"); } QDate MyMoneyOfxConnector::statementStartDate() const { if ((m_fiSettings.value("kmmofx-todayMinus").toInt() != 0) && !m_fiSettings.value("kmmofx-numRequestDays").isEmpty()) { return QDate::currentDate().addDays(-m_fiSettings.value("kmmofx-numRequestDays").toInt()); } else if ((m_fiSettings.value("kmmofx-lastUpdate").toInt() != 0) && !m_account.value("lastImportedTransactionDate").isEmpty()) { return QDate::fromString(m_account.value("lastImportedTransactionDate"), Qt::ISODate); } else if ((m_fiSettings.value("kmmofx-pickDate").toInt() != 0) && !m_fiSettings.value("kmmofx-specificDate").isEmpty()) { return QDate::fromString(m_fiSettings.value("kmmofx-specificDate")); } return QDate::currentDate().addMonths(-2); } OfxAccountData::AccountType MyMoneyOfxConnector::accounttype() const { OfxAccountData::AccountType result = OfxAccountData::OFX_CHECKING; QString type = m_account.onlineBankingSettings()["type"]; if (type == "CHECKING") result = OfxAccountData::OFX_CHECKING; else if (type == "SAVINGS") result = OfxAccountData::OFX_SAVINGS; else if (type == "MONEY MARKET") result = OfxAccountData::OFX_MONEYMRKT; else if (type == "CREDIT LINE") result = OfxAccountData::OFX_CREDITLINE; else if (type == "CMA") result = OfxAccountData::OFX_CMA; else if (type == "CREDIT CARD") result = OfxAccountData::OFX_CREDITCARD; else if (type == "INVESTMENT") result = OfxAccountData::OFX_INVESTMENT; else { switch (m_account.accountType()) { case eMyMoney::Account::Type::Investment: result = OfxAccountData::OFX_INVESTMENT; break; case eMyMoney::Account::Type::CreditCard: result = OfxAccountData::OFX_CREDITCARD; break; case eMyMoney::Account::Type::Savings: result = OfxAccountData::OFX_SAVINGS; break; default: break; } } // This is a bit of a personalized hack. Sometimes we may want to override the // ofx type for an account. For now, I will stash it in the notes! QRegExp rexp("OFXTYPE:([A-Z]*)"); if (rexp.indexIn(m_account.description()) != -1) { QString override = rexp.cap(1); qDebug() << "MyMoneyOfxConnector::accounttype() overriding to " << result; if (override == "BANK") result = OfxAccountData::OFX_CHECKING; else if (override == "CC") result = OfxAccountData::OFX_CREDITCARD; else if (override == "INV") result = OfxAccountData::OFX_INVESTMENT; else if (override == "MONEYMARKET") result = OfxAccountData::OFX_MONEYMRKT; } return result; } void MyMoneyOfxConnector::initRequest(OfxFiLogin* fi) const { memset(fi, 0, sizeof(OfxFiLogin)); strncpy(fi->fid, fiid().toLatin1(), OFX_FID_LENGTH - 1); strncpy(fi->org, fiorg().toLatin1(), OFX_ORG_LENGTH - 1); strncpy(fi->userid, username().toLatin1(), OFX_USERID_LENGTH - 1); strncpy(fi->userpass, password().toLatin1(), OFX_USERPASS_LENGTH - 1); #ifdef LIBOFX_HAVE_CLIENTUID strncpy(fi->clientuid, clientUid().toLatin1(), OFX_CLIENTUID_LENGTH - 1); #endif // If we don't know better, we pretend to be Quicken 2008 // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-intuit-products/ // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-microsoft-money/ QString appId = m_account.onlineBankingSettings().value("appId"); QRegExp exp("(.*):(.*)"); if (exp.indexIn(appId) != -1) { strncpy(fi->appid, exp.cap(1).toLatin1(), OFX_APPID_LENGTH - 1); strncpy(fi->appver, exp.cap(2).toLatin1(), OFX_APPVER_LENGTH - 1); } else { strncpy(fi->appid, "QWIN", OFX_APPID_LENGTH - 1); strncpy(fi->appver, "1700", OFX_APPVER_LENGTH - 1); } QString headerVersion = m_account.onlineBankingSettings().value("kmmofx-headerVersion"); if (!headerVersion.isEmpty()) { strncpy(fi->header_version, headerVersion.toLatin1(), OFX_HEADERVERSION_LENGTH - 1); } } const QByteArray MyMoneyOfxConnector::statementRequest() const { OfxFiLogin fi; initRequest(&fi); OfxAccountData account; memset(&account, 0, sizeof(OfxAccountData)); if (!iban().toLatin1().isEmpty()) { strncpy(account.bank_id, iban().toLatin1(), OFX_BANKID_LENGTH - 1); strncpy(account.broker_id, iban().toLatin1(), OFX_BROKERID_LENGTH - 1); } strncpy(account.account_number, accountnum().toLatin1(), OFX_ACCTID_LENGTH - 1); account.account_type = accounttype(); QByteArray result; if (fi.userpass[0]) { char *szrequest = libofx_request_statement(&fi, &account, QDateTime(statementStartDate()).toTime_t()); QString request = szrequest; // remove the trailing zero result = request.toUtf8(); if(result.at(result.size()-1) == 0) result.truncate(result.size() - 1); free(szrequest); } return result; } #if 0 MyMoneyOfxConnector::Tag MyMoneyOfxConnector::message(const QString& _msgType, const QString& _trnType, const Tag& _request) { return Tag(_msgType + "MSGSRQV1") .subtag(Tag(_trnType + "TRNRQ") .element("TRNUID", uuid()) .element("CLTCOOKIE", "1") .subtag(_request)); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::investmentRequest() const { QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); return message("INVSTMT", "INVSTMT", Tag("INVSTMTRQ") .subtag(Tag("INVACCTFROM").element("BROKERID", fiorg()).element("ACCTID", accountnum())) .subtag(Tag("INCTRAN").element("DTSTART", dtstart_string).element("INCLUDE", "Y")) .element("INCOO", "Y") .subtag(Tag("INCPOS").element("DTASOF", dtnow_string).element("INCLUDE", "Y")) .element("INCBAL", "Y")); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::bankStatementRequest(const QDate& _dtstart) const { QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); return message("BANK", "STMT", Tag("STMTRQ") .subtag(Tag("BANKACCTFROM").element("BANKID", iban()).element("ACCTID", accountnum()).element("ACCTTYPE", "CHECKING")) .subtag(Tag("INCTRAN").element("DTSTART", dtstart_string).element("INCLUDE", "Y"))); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::creditCardRequest(const QDate& _dtstart) const { QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); return message("CREDITCARD", "CCSTMT", Tag("CCSTMTRQ") .subtag(Tag("CCACCTFROM").element("ACCTID", accountnum())) .subtag(Tag("INCTRAN").element("DTSTART", dtstart_string).element("INCLUDE", "Y"))); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::signOn() const { QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); Tag fi("FI"); fi.element("ORG", fiorg()); if (!fiid().isEmpty()) fi.element("FID", fiid()); return Tag("SIGNONMSGSRQV1") .subtag(Tag("SONRQ") .element("DTCLIENT", dtnow_string) .element("USERID", username()) .element("USERPASS", password()) .element("LANGUAGE", "ENG") .subtag(fi) .element("APPID", "QWIN") .element("APPVER", "1100")); } QString MyMoneyOfxConnector::header() { return QString("OFXHEADER:100\r\n" "DATA:OFXSGML\r\n" "VERSION:102\r\n" "SECURITY:NONE\r\n" "ENCODING:USASCII\r\n" "CHARSET:1252\r\n" "COMPRESSION:NONE\r\n" "OLDFILEUID:NONE\r\n" "NEWFILEUID:%1\r\n" "\r\n").arg(uuid()); } QString MyMoneyOfxConnector::uuid() { static int id = 1; return QDateTime::currentDateTime().toString("yyyyMMdd-hhmmsszzz-") + QString::number(id++); } // // Methods to provide RESPONSES to OFX requests. This has no real use in // KMyMoney, but it's included for the purposes of unit testing. This way, I // can create a MyMoneyAccount, write it to an OFX file, import that OFX file, // and check that everything made it through the importer. // // It's also a far-off dream to write an OFX server using KMyMoney as a // backend. It really should not be that hard, and it would fill a void in // the open source software community. // const QByteArray MyMoneyOfxConnector::statementResponse(const QDate& _dtstart) const { QString request; if (accounttype() == "CC") request = header() + Tag("OFX").subtag(signOnResponse()).subtag(creditCardStatementResponse(_dtstart)); else if (accounttype() == "INV") request = header() + Tag("OFX").subtag(signOnResponse()).data(investmentStatementResponse(_dtstart)); else request = header() + Tag("OFX").subtag(signOnResponse()).subtag(bankStatementResponse(_dtstart)); // remove the trailing zero QByteArray result = request.utf8(); result.truncate(result.size() - 1); return result; } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::signOnResponse() const { QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); Tag sonrs("SONRS"); sonrs .subtag(Tag("STATUS") .element("CODE", "0") .element("SEVERITY", "INFO") .element("MESSAGE", "The operation succeeded.") ) .element("DTSERVER", dtnow_string) .element("LANGUAGE", "ENG"); Tag fi("FI"); if (!fiorg().isEmpty()) fi.element("ORG", fiorg()); if (!fiid().isEmpty()) fi.element("FID", fiid()); if (!fi.isEmpty()) sonrs.subtag(fi); return Tag("SIGNONMSGSRSV1").subtag(sonrs); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::messageResponse(const QString& _msgType, const QString& _trnType, const Tag& _response) { return Tag(_msgType + "MSGSRSV1") .subtag(Tag(_trnType + "TRNRS") .element("TRNUID", uuid()) .subtag(Tag("STATUS").element("CODE", "0").element("SEVERITY", "INFO")) .element("CLTCOOKIE", "1") .subtag(_response)); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::bankStatementResponse(const QDate& _dtstart) const { MyMoneyFile* file = MyMoneyFile::instance(); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString transactionlist; MyMoneyTransactionFilter filter; filter.setDateFilter(_dtstart, QDate::currentDate()); filter.addAccount(m_account.id()); QList transactions = file->transactionList(filter); QList::const_iterator it_transaction = transactions.begin(); while (it_transaction != transactions.end()) { transactionlist += transaction(*it_transaction); ++it_transaction; } return messageResponse("BANK", "STMT", Tag("STMTRS") .element("CURDEF", "USD") .subtag(Tag("BANKACCTFROM").element("BANKID", iban()).element("ACCTID", accountnum()).element("ACCTTYPE", "CHECKING")) .subtag(Tag("BANKTRANLIST").element("DTSTART", dtstart_string).element("DTEND", dtnow_string).data(transactionlist)) .subtag(Tag("LEDGERBAL").element("BALAMT", file->balance(m_account.id()).formatMoney(QString(), 2, false)).element("DTASOF", dtnow_string))); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::creditCardStatementResponse(const QDate& _dtstart) const { MyMoneyFile* file = MyMoneyFile::instance(); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString transactionlist; MyMoneyTransactionFilter filter; filter.setDateFilter(_dtstart, QDate::currentDate()); filter.addAccount(m_account.id()); QList transactions = file->transactionList(filter); QList::const_iterator it_transaction = transactions.begin(); while (it_transaction != transactions.end()) { transactionlist += transaction(*it_transaction); ++it_transaction; } return messageResponse("CREDITCARD", "CCSTMT", Tag("CCSTMTRS") .element("CURDEF", "USD") .subtag(Tag("CCACCTFROM").element("ACCTID", accountnum())) .subtag(Tag("BANKTRANLIST").element("DTSTART", dtstart_string).element("DTEND", dtnow_string).data(transactionlist)) .subtag(Tag("LEDGERBAL").element("BALAMT", file->balance(m_account.id()).formatMoney(QString(), 2, false)).element("DTASOF", dtnow_string))); } QString MyMoneyOfxConnector::investmentStatementResponse(const QDate& _dtstart) const { MyMoneyFile* file = MyMoneyFile::instance(); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString transactionlist; MyMoneyTransactionFilter filter; filter.setDateFilter(_dtstart, QDate::currentDate()); filter.addAccount(m_account.id()); filter.addAccount(m_account.accountList()); QList transactions = file->transactionList(filter); QList::const_iterator it_transaction = transactions.begin(); while (it_transaction != transactions.end()) { transactionlist += investmentTransaction(*it_transaction); ++it_transaction; } Tag securitylist("SECLIST"); QCStringList accountids = m_account.accountList(); QCStringList::const_iterator it_accountid = accountids.begin(); while (it_accountid != accountids.end()) { MyMoneySecurity equity = file->security(file->account(*it_accountid).currencyId()); securitylist.subtag(Tag("STOCKINFO") .subtag(Tag("SECINFO") .subtag(Tag("SECID") .element("UNIQUEID", equity.id()) .element("UNIQUEIDTYPE", "KMYMONEY")) .element("SECNAME", equity.name()) .element("TICKER", equity.tradingSymbol()) .element("FIID", equity.id()))); ++it_accountid; } return messageResponse("INVSTMT", "INVSTMT", Tag("INVSTMTRS") .element("DTASOF", dtstart_string) .element("CURDEF", "USD") .subtag(Tag("INVACCTFROM").element("BROKERID", fiorg()).element("ACCTID", accountnum())) .subtag(Tag("INVTRANLIST").element("DTSTART", dtstart_string).element("DTEND", dtnow_string).data(transactionlist)) ) + Tag("SECLISTMSGSRSV1").subtag(securitylist); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::transaction(const MyMoneyTransaction& _t) const { // This method creates a transaction tag using ONLY the elements that importer uses MyMoneyFile* file = MyMoneyFile::instance(); //Use this version for bank/cc transactions MyMoneySplit s = _t.splitByAccount(m_account.id(), true); //TODO (Ace) Write "investmentTransaction()"... //Use this version for inv transactions //MyMoneySplit s = _t.splitByAccount( m_account.accountList(), true ); Tag result("STMTTRN"); result // This is a temporary hack. I don't use the trntype field in importing at all, // but libofx requires it to be there in order to import the file. .element("TRNTYPE", "DEBIT") .element("DTPOSTED", _t.postDate().toString(Qt::ISODate).remove(QRegExp("[^0-9]"))) .element("TRNAMT", s.value().formatMoney(QString(), 2, false)); if (! _t.bankID().isEmpty()) result.element("FITID", _t.bankID()); else result.element("FITID", _t.id()); if (! s.number().isEmpty()) result.element("CHECKNUM", s.number()); if (! s.payeeId().isEmpty()) result.element("NAME", file->payee(s.payeeId()).name()); if (! _t.memo().isEmpty()) result.element("MEMO", _t.memo()); return result; } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::investmentTransaction(const MyMoneyTransaction& _t) const { MyMoneyFile* file = MyMoneyFile::instance(); //Use this version for inv transactions MyMoneySplit s = _t.splitByAccount(m_account.accountList(), true); QByteArray stockid = file->account(s.accountId()).currencyId(); Tag invtran("INVTRAN"); invtran.element("FITID", _t.id()).element("DTTRADE", _t.postDate().toString(Qt::ISODate).remove(QRegExp("[^0-9]"))); if (!_t.memo().isEmpty()) invtran.element("MEMO", _t.memo()); if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { if (s.shares().isNegative()) { return Tag("SELLSTOCK") .subtag(Tag("INVSELL") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("UNITS", QString(((s.shares())).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("UNITPRICE", QString((s.value() / s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.]"))) .element("TOTAL", QString((-s.value()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("SUBACCTSEC", "CASH") .element("SUBACCTFUND", "CASH")) .element("SELLTYPE", "SELL"); } else { return Tag("BUYSTOCK") .subtag(Tag("INVBUY") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("UNITS", QString((s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("UNITPRICE", QString((s.value() / s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.]"))) .element("TOTAL", QString((-(s.value())).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("SUBACCTSEC", "CASH") .element("SUBACCTFUND", "CASH")) .element("BUYTYPE", "BUY"); } } else if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { // Should the TOTAL tag really be negative for a REINVEST? That's very strange, but // it's what they look like coming from my bank, and I can't find any information to refute it. return Tag("REINVEST") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("INCOMETYPE", "DIV") .element("TOTAL", QString((-s.value()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("SUBACCTSEC", "CASH") .element("UNITS", QString((s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("UNITPRICE", QString((s.value() / s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.]"))); } else if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { // find the split with the category, which has the actual amount of the dividend QList splits = _t.splits(); QList::const_iterator it_split = splits.begin(); bool found = false; while (it_split != splits.end()) { QByteArray accid = (*it_split).accountId(); MyMoneyAccount acc = file->account(accid); if (acc.accountType() == eMyMoney::Account::Type::Income || acc.accountType() == eMyMoney::Account::Type::Expense) { found = true; break; } ++it_split; } if (found) return Tag("INCOME") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("INCOMETYPE", "DIV") .element("TOTAL", QString((-(*it_split).value()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9\\.\\-]"))) .element("SUBACCTSEC", "CASH") .element("SUBACCTFUND", "CASH"); else return Tag("ERROR").element("DETAILS", "Unable to determine the amount of this income transaction."); } //FIXME: Do something useful with these errors return Tag("ERROR").element("DETAILS", "This transaction contains an unsupported action type"); } #endif KWallet::Wallet *openSynchronousWallet() { using KWallet::Wallet; // first handle the simple case in which we already use the wallet but need the object again in // this case the wallet access permission dialog will no longer appear so we don't need to pass // a valid window id or do anything special since the function call should return immediately const bool alreadyUsingTheWallet = Wallet::users(Wallet::NetworkWallet()).contains("KMyMoney"); if (alreadyUsingTheWallet) { return Wallet::openWallet(Wallet::NetworkWallet(), 0, Wallet::Synchronous); } - // search for a suitable parent for the wallet than needs to be deactivated while the + // search for a suitable parent for the wallet that needs to be deactivated while the // wallet access permission dialog is not dismissed with either accept or reject KWallet::Wallet *wallet = 0; QWidget *parentWidgetForWallet = 0; if (qApp->activeModalWidget()) { parentWidgetForWallet = qApp->activeModalWidget(); } else if (qApp->activeWindow()) { parentWidgetForWallet = qApp->activeWindow(); } else { QList mainWindowList = KMainWindow::memberList(); if (!mainWindowList.isEmpty()) parentWidgetForWallet = mainWindowList.front(); } // only open the wallet synchronously if we have a valid parent otherwise crashes could occur if (parentWidgetForWallet) { // while the wallet is being opened disable the widget to prevent input processing const bool enabled = parentWidgetForWallet->isEnabled(); parentWidgetForWallet->setEnabled(false); wallet = Wallet::openWallet(Wallet::NetworkWallet(), parentWidgetForWallet->winId(), Wallet::Synchronous); parentWidgetForWallet->setEnabled(enabled); } return wallet; } diff --git a/kmymoney/plugins/ofx/import/ofximporter.cpp b/kmymoney/plugins/ofx/import/ofximporter.cpp index eab6859e5..ff0f5dcda 100644 --- a/kmymoney/plugins/ofx/import/ofximporter.cpp +++ b/kmymoney/plugins/ofx/import/ofximporter.cpp @@ -1,878 +1,883 @@ -/*************************************************************************** - ofximporter.cpp - ------------------- - begin : Sat Jan 01 2005 - copyright : (C) 2005 by Ace Jones - email : Ace Jones - ***************************************************************************/ - -/*************************************************************************** - * * - * 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. * - * * - ***************************************************************************/ +/* + * Copyright 2005 Ace Jones acejones@users.sourceforge.net + * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "ofximporter.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "konlinebankingstatus.h" #include "konlinebankingsetupwizard.h" #include "kofxdirectconnectdlg.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneystatement.h" +#include "mymoneystatementreader.h" #include "statementinterface.h" #include "importinterface.h" #include "viewinterface.h" #include "ui_importoption.h" +#include "kmymoneyutils.h" //#define DEBUG_LIBOFX using KWallet::Wallet; class OFXImporter::Private { public: Private() : m_valid(false), m_preferName(PreferId), m_walletIsOpen(false), m_statusDlg(0), m_wallet(0), m_updateStartDate(QDate(1900,1,1)), m_timestampOffset(0) {} bool m_valid; enum NamePreference { PreferId = 0, PreferName, PreferMemo } m_preferName; bool m_walletIsOpen; QList m_statementlist; QList m_securitylist; QString m_fatalerror; QStringList m_infos; QStringList m_warnings; QStringList m_errors; KOnlineBankingStatus* m_statusDlg; Wallet *m_wallet; QDate m_updateStartDate; int m_timestampOffset; }; OFXImporter::OFXImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "ofximporter"), /* * the string in the line above must be the same as * X-KDE-PluginInfo-Name and the provider name assigned in * OfxImporterPlugin::onlineBankingSettings() */ KMyMoneyPlugin::ImporterPlugin(), d(new Private) { Q_UNUSED(args) setComponentName(QStringLiteral("ofximporter"), i18n("OFX Importer")); setXMLFile(QStringLiteral("ofximporter.rc")); createActions(); // For ease announce that we have been loaded. qDebug("Plugins: ofximporter loaded"); } OFXImporter::~OFXImporter() { delete d; qDebug("Plugins: ofximporter unloaded"); } void OFXImporter::createActions() { const auto &kpartgui = QStringLiteral("file_import_ofx"); auto a = actionCollection()->addAction(kpartgui); a->setText(i18n("OFX...")); connect(a, &QAction::triggered, this, static_cast(&OFXImporter::slotImportFile)); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void OFXImporter::slotImportFile() { QWidget * widget = new QWidget; Ui_ImportOption* option = new Ui_ImportOption; option->setupUi(widget); QUrl url = importInterface()->selectFile(i18n("OFX import file selection"), QString(), QStringLiteral("*.ofx *.qfx *.ofc|OFX files (*.ofx *.qfx *.ofc);;*|All files (*)"), QFileDialog::ExistingFile, widget); d->m_preferName = static_cast(option->m_preferName->currentIndex()); if (url.isValid()) { if (isMyFormat(url.path())) { + statementInterface()->resetMessages(); slotImportFile(url.path()); + statementInterface()->showMessages(d->m_statementlist.count()); } else { KMessageBox::error(0, i18n("Unable to import %1 using the OFX importer plugin. This file is not the correct format.", url.toDisplayString()), i18n("Incorrect format")); } } delete option; delete widget; } QString OFXImporter::formatName() const { return QStringLiteral("OFX"); } QString OFXImporter::formatFilenameFilter() const { return QStringLiteral("*.ofx *.qfx *.ofc"); } bool OFXImporter::isMyFormat(const QString& filename) const { // filename is considered an Ofx file if it contains // the tag "" or "" in the first 20 lines. // which contain some data bool result = false; QFile f(filename); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream ts(&f); int lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { // get a line of data and remove all unnecessary whitepace chars QString line = ts.readLine().simplified(); if (line.contains(QStringLiteral(""), Qt::CaseInsensitive) || line.contains(QStringLiteral(""), Qt::CaseInsensitive)) result = true; // count only lines that contain some non white space chars if (!line.isEmpty()) lineCount--; } f.close(); } return result; } bool OFXImporter::import(const QString& filename) { d->m_fatalerror = i18n("Unable to parse file"); d->m_valid = false; d->m_errors.clear(); d->m_warnings.clear(); d->m_infos.clear(); d->m_statementlist.clear(); d->m_securitylist.clear(); QByteArray filename_deep = QFile::encodeName(filename); ofx_STATUS_msg = true; ofx_INFO_msg = true; ofx_WARNING_msg = true; ofx_ERROR_msg = true; #ifdef DEBUG_LIBOFX ofx_PARSER_msg = true; ofx_DEBUG_msg = true; ofx_DEBUG1_msg = true; ofx_DEBUG2_msg = true; ofx_DEBUG3_msg = true; ofx_DEBUG4_msg = true; ofx_DEBUG5_msg = true; #endif LibofxContextPtr ctx = libofx_get_new_context(); Q_CHECK_PTR(ctx); qDebug("setup callback routines"); ofx_set_transaction_cb(ctx, ofxTransactionCallback, this); ofx_set_statement_cb(ctx, ofxStatementCallback, this); ofx_set_account_cb(ctx, ofxAccountCallback, this); ofx_set_security_cb(ctx, ofxSecurityCallback, this); ofx_set_status_cb(ctx, ofxStatusCallback, this); qDebug("process data"); libofx_proc_file(ctx, filename_deep, AUTODETECT); libofx_free_context(ctx); if (d->m_valid) { d->m_fatalerror.clear(); d->m_valid = storeStatements(d->m_statementlist); } return d->m_valid; } QString OFXImporter::lastError() const { if (d->m_errors.count() == 0) return d->m_fatalerror; return d->m_errors.join(QStringLiteral("

")); } /* __________________________________________________________________________ * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA * * Static callbacks for LibOFX * * YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY */ int OFXImporter::ofxTransactionCallback(struct OfxTransactionData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); MyMoneyStatement::Transaction t; if (data.date_posted_valid) { QDateTime dt; dt.setTime_t(data.date_posted - pofx->d->m_timestampOffset * 60); t.m_datePosted = dt.date(); } else if (data.date_initiated_valid) { QDateTime dt; dt.setTime_t(data.date_initiated - pofx->d->m_timestampOffset * 60); t.m_datePosted = dt.date(); } if (t.m_datePosted.isValid()) { // verify the transaction date is one we want if (t.m_datePosted < pofx->d->m_updateStartDate) { //kDebug(0) << "discarding transaction dated" << qPrintable(t.m_datePosted.toString(Qt::ISODate)); return 0; } } if (data.amount_valid) { t.m_amount = MyMoneyMoney(data.amount, 1000); } if (data.check_number_valid) { t.m_strNumber = QString::fromUtf8(data.check_number); } if (data.fi_id_valid) { t.m_strBankID = QStringLiteral("ID ") + QString::fromUtf8(data.fi_id); } else if (data.reference_number_valid) { t.m_strBankID = QStringLiteral("REF ") + QString::fromUtf8(data.reference_number); } // Decide whether to use NAME, PAYEEID or MEMO to construct the payee bool validity[3] = {false, false, false}; QStringList values; switch (pofx->d->m_preferName) { case OFXImporter::Private::PreferId: // PAYEEID default: validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.memo); break; case OFXImporter::Private::PreferName: // NAME validity[0] = data.name_valid; validity[1] = data.payee_id_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.memo); break; case OFXImporter::Private::PreferMemo: // MEMO validity[0] = data.memo_valid; validity[1] = data.payee_id_valid; validity[2] = data.name_valid; values += QString::fromUtf8(data.memo); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); break; } // for investment transactions we don't use the meme as payee if (data.invtransactiontype_valid) { values.clear(); validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = false; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); } for (int idx = 0; idx < 3; ++idx) { if (validity[idx]) { t.m_strPayee = values[idx]; break; } } // extract memo field if we haven't used it as payee if ((data.memo_valid) && (pofx->d->m_preferName != OFXImporter::Private::PreferMemo)) { t.m_strMemo = QString::fromUtf8(data.memo); } // If the payee or memo fields are blank, set them to // the other one which is NOT blank. (acejones) if (t.m_strPayee.isEmpty()) { // But we only create a payee for non-investment transactions (ipwizard) if (! t.m_strMemo.isEmpty() && data.invtransactiontype_valid == false) t.m_strPayee = t.m_strMemo; } else { if (t.m_strMemo.isEmpty()) t.m_strMemo = t.m_strPayee; } if (data.security_data_valid) { struct OfxSecurityData* secdata = data.security_data_ptr; if (secdata->ticker_valid) { t.m_strSymbol = QString::fromUtf8(secdata->ticker); } if (secdata->secname_valid) { t.m_strSecurity = QString::fromUtf8(secdata->secname); } } t.m_shares = MyMoneyMoney(); if (data.units_valid) { t.m_shares = MyMoneyMoney(data.units, 100000).reduce(); } t.m_price = MyMoneyMoney(); if (data.unitprice_valid) { t.m_price = MyMoneyMoney(data.unitprice, 100000).reduce(); } t.m_fees = MyMoneyMoney(); if (data.fees_valid) { t.m_fees += MyMoneyMoney(data.fees, 1000).reduce(); } if (data.commission_valid) { t.m_fees += MyMoneyMoney(data.commission, 1000).reduce(); } bool unhandledtype = false; QString type; if (data.invtransactiontype_valid) { switch (data.invtransactiontype) { case OFX_BUYDEBT: case OFX_BUYMF: case OFX_BUYOPT: case OFX_BUYOTHER: case OFX_BUYSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Buy; break; case OFX_REINVEST: t.m_eAction = eMyMoney::Transaction::Action::ReinvestDividend; break; case OFX_SELLDEBT: case OFX_SELLMF: case OFX_SELLOPT: case OFX_SELLOTHER: case OFX_SELLSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Sell; break; case OFX_INCOME: t.m_eAction = eMyMoney::Transaction::Action::CashDividend; // NOTE: With CashDividend, the amount of the dividend should // be in data.amount. Since I've never seen an OFX file with // cash dividends, this is an assumption on my part. (acejones) break; // // These types are all not handled. We will generate a warning for them. // case OFX_CLOSUREOPT: unhandledtype = true; type = QStringLiteral("CLOSUREOPT (Close a position for an option)"); break; case OFX_INVEXPENSE: unhandledtype = true; type = QStringLiteral("INVEXPENSE (Misc investment expense that is associated with a specific security)"); break; case OFX_JRNLFUND: unhandledtype = true; type = QStringLiteral("JRNLFUND (Journaling cash holdings between subaccounts within the same investment account)"); break; case OFX_MARGININTEREST: unhandledtype = true; type = QStringLiteral("MARGININTEREST (Margin interest expense)"); break; case OFX_RETOFCAP: unhandledtype = true; type = QStringLiteral("RETOFCAP (Return of capital)"); break; case OFX_SPLIT: unhandledtype = true; type = QStringLiteral("SPLIT (Stock or mutial fund split)"); break; case OFX_TRANSFER: unhandledtype = true; type = QStringLiteral("TRANSFER (Transfer holdings in and out of the investment account)"); break; default: unhandledtype = true; type = QString("UNKNOWN %1").arg(data.invtransactiontype); break; } } else t.m_eAction = eMyMoney::Transaction::Action::None; // In the case of investment transactions, the 'total' is supposed to the total amount // of the transaction. units * unitprice +/- commission. Easy, right? Sadly, it seems // some ofx creators do not follow this in all circumstances. Therefore, we have to double- // check the total here and adjust it if it's wrong. #if 0 // Even more sadly, this logic is BROKEN. It consistently results in bogus total // values, because of rounding errors in the price. A more through solution would // be to test if the comission alone is causing a discrepency, and adjust in that case. if (data.invtransactiontype_valid && data.unitprice_valid) { double proper_total = t.m_dShares * data.unitprice + t.m_moneyFees; if (proper_total != t.m_moneyAmount) { pofx->addWarning(QString("Transaction %1 has an incorrect total of %2. Using calculated total of %3 instead.").arg(t.m_strBankID).arg(t.m_moneyAmount).arg(proper_total)); t.m_moneyAmount = proper_total; } } #endif if (unhandledtype) pofx->addWarning(QString("Transaction %1 has an unsupported type (%2).").arg(t.m_strBankID, type)); else s.m_listTransactions += t; // kDebug(2) << Q_FUNC_INFO << "return 0 "; return 0; } int OFXImporter::ofxStatementCallback(struct OfxStatementData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); pofx->setValid(); if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_id_valid) { s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.date_start_valid) { QDateTime dt; dt.setTime_t(data.date_start - pofx->d->m_timestampOffset * 60); s.m_dateBegin = dt.date(); } if (data.date_end_valid) { QDateTime dt; dt.setTime_t(data.date_end - pofx->d->m_timestampOffset * 60); s.m_dateEnd = dt.date(); } if (data.ledger_balance_valid && data.ledger_balance_date_valid) { s.m_closingBalance = MyMoneyMoney(data.ledger_balance); QDateTime dt; dt.setTime_t(data.ledger_balance_date); s.m_dateEnd = dt.date(); } // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OFXImporter::ofxAccountCallback(struct OfxAccountData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); pofx->addnew(); MyMoneyStatement& s = pofx->back(); // Having any account at all makes an ofx statement valid pofx->d->m_valid = true; if (data.account_id_valid) { s.m_strAccountName = QString::fromUtf8(data.account_name); s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.bank_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.bank_id); } if (data.broker_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.broker_id); } if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_type_valid) { switch (data.account_type) { case OfxAccountData::OFX_CHECKING : s.m_eType = eMyMoney::Statement::Type::Checkings; break; case OfxAccountData::OFX_SAVINGS : s.m_eType = eMyMoney::Statement::Type::Savings; break; case OfxAccountData::OFX_MONEYMRKT : s.m_eType = eMyMoney::Statement::Type::Investment; break; case OfxAccountData::OFX_CREDITLINE : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CMA : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CREDITCARD : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_INVESTMENT : s.m_eType = eMyMoney::Statement::Type::Investment; break; } } // ask KMyMoney for an account id s.m_accountId = pofx->account(QStringLiteral("kmmofx-acc-ref"), QString("%1-%2").arg(s.m_strRoutingNumber, s.m_strAccountNumber)).id(); // copy over the securities s.m_listSecurities = pofx->d->m_securitylist; // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OFXImporter::ofxSecurityCallback(struct OfxSecurityData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement::Security sec; if (data.unique_id_valid) { sec.m_strId = QString::fromUtf8(data.unique_id); } if (data.secname_valid) { sec.m_strName = QString::fromUtf8(data.secname); } if (data.ticker_valid) { sec.m_strSymbol = QString::fromUtf8(data.ticker); } pofx->d->m_securitylist += sec; return 0; } int OFXImporter::ofxStatusCallback(struct OfxStatusData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); QString message; // if we got this far, we know we were able to parse the file. // so if it fails after here it can only because there were no actual // accounts in the file! pofx->d->m_fatalerror = i18n("No accounts found."); if (data.ofx_element_name_valid) message.prepend(QString("%1: ").arg(QString::fromUtf8(data.ofx_element_name))); if (data.code_valid) message += QString("%1 (Code %2): %3").arg(QString::fromUtf8(data.name)).arg(data.code).arg(QString::fromUtf8(data.description)); if (data.server_message_valid) message += QString(" (%1)").arg(QString::fromUtf8(data.server_message)); if (data.severity_valid) { switch (data.severity) { case OfxStatusData::INFO: pofx->addInfo(message); break; case OfxStatusData::ERROR: pofx->addError(message); break; case OfxStatusData::WARN: pofx->addWarning(message); break; default: pofx->addWarning(message); pofx->addWarning(QStringLiteral("Previous message was an unknown type. 'WARNING' was assumed.")); break; } } // kDebug(2) << Q_FUNC_INFO << " return 0 "; return 0; } QStringList OFXImporter::importStatement(const MyMoneyStatement &s) { qDebug("OfxImporterPlugin::importStatement start"); return statementInterface()->import(s, false); } MyMoneyAccount OFXImporter::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void OFXImporter::protocols(QStringList& protocolList) const { protocolList.clear(); protocolList << QStringLiteral("OFX"); } QWidget* OFXImporter::accountConfigTab(const MyMoneyAccount& acc, QString& name) { name = i18n("Online settings"); d->m_statusDlg = new KOnlineBankingStatus(acc, 0); return d->m_statusDlg; } MyMoneyKeyValueContainer OFXImporter::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); // keep the provider name in sync with the one found in kmm_ofximport.desktop kvp[QStringLiteral("provider")] = objectName().toLower(); if (d->m_statusDlg) { kvp.deletePair(QStringLiteral("appId")); kvp.deletePair(QStringLiteral("kmmofx-headerVersion")); kvp.deletePair(QStringLiteral("password")); d->m_wallet = openSynchronousWallet(); if (d->m_wallet && (d->m_wallet->hasFolder(KWallet::Wallet::PasswordFolder()) || d->m_wallet->createFolder(KWallet::Wallet::PasswordFolder())) && d->m_wallet->setFolder(KWallet::Wallet::PasswordFolder())) { QString key = OFX_PASSWORD_KEY(kvp.value(QStringLiteral("url")), kvp.value(QStringLiteral("uniqueId"))); if (d->m_statusDlg->m_storePassword->isChecked()) { d->m_wallet->writePassword(key, d->m_statusDlg->m_password->text()); } else { if (d->m_wallet->hasEntry(key)) { d->m_wallet->removeEntry(key); } } } else { if (d->m_statusDlg->m_storePassword->isChecked()) { kvp.setValue(QStringLiteral("password"), d->m_statusDlg->m_password->text()); } } if (!d->m_statusDlg->appId().isEmpty()) kvp.setValue(QStringLiteral("appId"), d->m_statusDlg->appId()); kvp.setValue(QStringLiteral("kmmofx-headerVersion"), d->m_statusDlg->headerVersion()); kvp.setValue(QStringLiteral("kmmofx-numRequestDays"), QString::number(d->m_statusDlg->m_numdaysSpin->value())); kvp.setValue(QStringLiteral("kmmofx-todayMinus"), QString::number(d->m_statusDlg->m_todayRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-lastUpdate"), QString::number(d->m_statusDlg->m_lastUpdateRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-pickDate"), QString::number(d->m_statusDlg->m_pickDateRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-specificDate"), d->m_statusDlg->m_specificDate->date().toString()); kvp.setValue(QStringLiteral("kmmofx-preferName"), QString::number(d->m_statusDlg->m_preferredPayee->currentIndex())); if (!d->m_statusDlg->m_clientUidEdit->text().isEmpty()) kvp.setValue(QStringLiteral("clientUid"), d->m_statusDlg->m_clientUidEdit->text()); else kvp.deletePair(QStringLiteral("clientUid")); if (d->m_statusDlg->m_timestampOffset->time().msecsSinceStartOfDay() == 0) { kvp.deletePair(QStringLiteral("kmmofx-timestampOffset")); } else { // get offset in minutes int offset = d->m_statusDlg->m_timestampOffset->time().msecsSinceStartOfDay() / 1000 / 60; if (d->m_statusDlg->m_timestampOffsetSign->currentText() == QStringLiteral("-")) { offset = -offset; } kvp.setValue(QStringLiteral("kmmofx-timestampOffset"), QString::number(offset)); } // get rid of pre 4.6 values kvp.deletePair(QStringLiteral("kmmofx-preferPayeeid")); } return kvp; } bool OFXImporter::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { Q_UNUSED(acc); bool rc = false; QPointer wiz = new KOnlineBankingSetupWizard(0); if (wiz->isInit()) { if (wiz->exec() == QDialog::Accepted) { rc = wiz->chosenSettings(settings); } } delete wiz; return rc; } bool OFXImporter::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { Q_UNUSED(moreAccounts); qDebug("OfxImporterPlugin::updateAccount"); try { if (!acc.id().isEmpty()) { // Save the value of preferName to be used by ofxTransactionCallback d->m_preferName = static_cast(acc.onlineBankingSettings().value(QStringLiteral("kmmofx-preferName")).toInt()); QPointer dlg = new KOfxDirectConnectDlg(acc); connect(dlg.data(), &KOfxDirectConnectDlg::statementReady, this, static_cast(&OFXImporter::slotImportFile)); // get the date of the earliest transaction that we are interested in // from the settings for this account MyMoneyKeyValueContainer settings = acc.onlineBankingSettings(); if (!settings.value(QStringLiteral("provider")).isEmpty()) { if ((settings.value(QStringLiteral("kmmofx-todayMinus")).toInt() != 0) && !settings.value(QStringLiteral("kmmofx-numRequestDays")).isEmpty()) { //kDebug(0) << "start date = today minus"; d->m_updateStartDate = QDate::currentDate().addDays(-settings.value(QStringLiteral("kmmofx-numRequestDays")).toInt()); } else if ((settings.value(QStringLiteral("kmmofx-lastUpdate")).toInt() != 0) && !acc.value(QStringLiteral("lastImportedTransactionDate")).isEmpty()) { //kDebug(0) << "start date = last update"; d->m_updateStartDate = QDate::fromString(acc.value(QStringLiteral("lastImportedTransactionDate")), Qt::ISODate); } else if ((settings.value(QStringLiteral("kmmofx-pickDate")).toInt() != 0) && !settings.value(QStringLiteral("kmmofx-specificDate")).isEmpty()) { //kDebug(0) << "start date = pick date"; d->m_updateStartDate = QDate::fromString(settings.value(QStringLiteral("kmmofx-specificDate"))); } else { //kDebug(0) << "start date = today - 2 months"; d->m_updateStartDate = QDate::currentDate().addMonths(-2); } } d->m_timestampOffset = settings.value("kmmofx-timestampOffset").toInt(); //kDebug(0) << "ofx plugin: account" << acc.name() << "earliest transaction date to process =" << qPrintable(d->m_updateStartDate.toString(Qt::ISODate)); if (dlg->init()) dlg->exec(); delete dlg; // reset the earliest-interesting-transaction date to the non-specific account setting d->m_updateStartDate = QDate(1900,1,1); d->m_timestampOffset = 0; } } catch (const MyMoneyException &e) { KMessageBox::information(0 , i18n("Error connecting to bank: %1", QString::fromLatin1(e.what()))); } return false; } void OFXImporter::slotImportFile(const QString& url) { qDebug("OfxImporterPlugin::slotImportFile"); if (!import(url)) { KMessageBox::error(0, QString("%1").arg(i18n("

Unable to import '%1' using the OFX importer plugin. The plugin returned the following error:

%2

", url, lastError())), i18n("Importing error")); } } bool OFXImporter::storeStatements(const QList &statements) { if (statements.isEmpty()) return true; auto ok = true; auto abort = false; // FIXME Deal with warnings/errors coming back from plugins /*if ( ofx.errors().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following errors were returned from your bank"),ofx.errors(),i18n("OFX Errors")) == KMessageBox::Cancel ) abort = true; } if ( ofx.warnings().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following warnings were returned from your bank"),ofx.warnings(),i18n("OFX Warnings"),KStandardGuiItem::cont(),"ofxwarnings") == KMessageBox::Cancel ) abort = true; }*/ qDebug("OfxImporterPlugin::storeStatements() with %d statements called", statements.count()); for (const auto& statement : statements) { if (abort) break; if (importStatement(statement).isEmpty()) ok = false; } if (!ok) KMessageBox::error(nullptr, i18n("Importing process terminated unexpectedly."), i18n("Failed to import all statements.")); return ok; } void OFXImporter::addnew() { d->m_statementlist.push_back(MyMoneyStatement()); } MyMoneyStatement& OFXImporter::back() { return d->m_statementlist.back(); } bool OFXImporter::isValid() const { return d->m_valid; } void OFXImporter::setValid() { d->m_valid = true; } void OFXImporter::addInfo(const QString& _msg) { d->m_infos += _msg; } void OFXImporter::addWarning(const QString& _msg) { d->m_warnings += _msg; } void OFXImporter::addError(const QString& _msg) { d->m_errors += _msg; } const QStringList& OFXImporter::infos() const // krazy:exclude=spelling { return d->m_infos; } const QStringList& OFXImporter::warnings() const { return d->m_warnings; } const QStringList& OFXImporter::errors() const { return d->m_errors; } K_PLUGIN_FACTORY_WITH_JSON(OFXImporterFactory, "ofximporter.json", registerPlugin();) #include "ofximporter.moc" diff --git a/kmymoney/plugins/ofx/import/ofximporter.h b/kmymoney/plugins/ofx/import/ofximporter.h index 7b9df7506..96b1cd5ed 100644 --- a/kmymoney/plugins/ofx/import/ofximporter.h +++ b/kmymoney/plugins/ofx/import/ofximporter.h @@ -1,152 +1,152 @@ -/*************************************************************************** - ofximporter.h - ------------------- - begin : Sat Jan 01 2005 - copyright : (C) 2005 by Ace Jones - email : Ace Jones - ***************************************************************************/ - -/*************************************************************************** - * * - * 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. * - * * - ***************************************************************************/ - +/* + * Copyright 2005 Ace Jones acejones@users.sourceforge.net + * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef OFXIMPORTER_H #define OFXIMPORTER_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // Library Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyplugin.h" #include "mymoneykeyvaluecontainer.h" /** @author Ace Jones */ class MyMoneyAccount; class MyMoneyStatement; class OFXImporter : public KMyMoneyPlugin::Plugin, public KMyMoneyPlugin::ImporterPlugin, public KMyMoneyPlugin::OnlinePlugin { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::ImporterPlugin) Q_INTERFACES(KMyMoneyPlugin::OnlinePlugin) public: explicit OFXImporter(QObject *parent, const QVariantList &args); ~OFXImporter() override; /** * This method returns the english-language name of the format * this plugin imports, e.g. "OFX" * * @return QString Name of the format */ QString formatName() const override; /** * This method returns the filename filter suitable for passing to * KFileDialog::setFilter(), e.g. "*.ofx *.qfx" which describes how * files of this format are likely to be named in the file system * * @return QString Filename filter string */ QString formatFilenameFilter() const override; /** * This method returns whether this plugin is able to import * a particular file. * * @param filename Fully-qualified pathname to a file * * @return bool Whether the indicated file is importable by this plugin */ bool isMyFormat(const QString& filename) const override; /** * Import a file * * @param filename File to import * * @return bool Whether the import was successful. */ bool import(const QString& filename) override; /** * Returns the error result of the last import * * @return QString English-language name of the error encountered in the * last import, or QString() if it was successful. * */ QString lastError() const override; /** * Returns a pointer to a widget that will be added as tab to * the account edit dialog. @sa KNewAccountDlg. The data of the * current account is passed as const reference @a acc. @a name references * a QString that will receive the name of the tab to be shown in the dialog. */ QWidget* accountConfigTab(const MyMoneyAccount& acc, QString& name) override; /** * Retrieves the online banking settings and updates the password in the KDE wallet. * The caller has the choice to pass a MyMoneyKeyValueContainer with the @a current * settings. Only those are modified that are used by the plugin. */ MyMoneyKeyValueContainer onlineBankingSettings(const MyMoneyKeyValueContainer& current) override; MyMoneyAccount account(const QString& key, const QString& value) const; void protocols(QStringList& protocolList) const override; bool mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) override; bool updateAccount(const MyMoneyAccount& acc, bool moreAccounts) override; protected Q_SLOTS: void slotImportFile(); void slotImportFile(const QString& url); protected: void createActions(); void addnew(); MyMoneyStatement& back(); bool isValid() const; void setValid(); void addInfo(const QString& _msg); void addWarning(const QString& _msg); void addError(const QString& _msg); const QStringList& infos() const; // krazy:exclude=spelling const QStringList& warnings() const; const QStringList& errors() const; bool storeStatements(const QList &statements); QStringList importStatement(const MyMoneyStatement &s); static int ofxTransactionCallback(struct OfxTransactionData, void*); static int ofxStatementCallback(struct OfxStatementData, void*); static int ofxAccountCallback(struct OfxAccountData, void*); static int ofxStatusCallback(struct OfxStatusData, void*); static int ofxSecurityCallback(struct OfxSecurityData, void*); private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; }; #endif diff --git a/kmymoney/plugins/qif/import/mymoneyqifreader.cpp b/kmymoney/plugins/qif/import/mymoneyqifreader.cpp index 4da5f1e42..d90a49c0c 100644 --- a/kmymoney/plugins/qif/import/mymoneyqifreader.cpp +++ b/kmymoney/plugins/qif/import/mymoneyqifreader.cpp @@ -1,2083 +1,2088 @@ /*************************************************************************** mymoneyqifreader.cpp ------------------- begin : Mon Jan 27 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneyqifreader.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include "kjobwidgets.h" #include "kio/job.h" // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "mymoneystatement.h" // define this to debug the code. Using external filters // while debugging did not work too good for me, so I added // this code. // #define DEBUG_IMPORT #ifdef DEBUG_IMPORT #ifdef __GNUC__ #warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!" #endif #endif class MyMoneyQifReader::Private { public: Private() : accountType(eMyMoney::Account::Type::Checkings), firstTransaction(true), mapCategories(true), transactionType(MyMoneyQifReader::QifEntryTypeE::EntryUnknown) {} const QString accountTypeToQif(eMyMoney::Account::Type type) const; /** * finalize the current statement and add it to the statement list */ void finishStatement(); bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim); /** * Converts the QIF specific N-record of investment transactions into * a category name */ const QString typeToAccountName(const QString& type) const; /** * Converts the QIF reconcile state to the KMyMoney reconcile state */ eMyMoney::Split::State reconcileState(const QString& state) const; /** */ void fixMultiLineMemo(QString& memo) const; public: /** * the statement that is currently collected/processed */ MyMoneyStatement st; /** * the list of all statements to be sent to MyMoneyStatementReader */ QList statements; /** * a list of already used hashes in this file */ QMap m_hashMap; QString st_AccountName; QString st_AccountId; eMyMoney::Account::Type accountType; bool firstTransaction; bool mapCategories; MyMoneyQifReader::QifEntryTypeE transactionType; }; void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const { memo.replace("\\n", "\n"); } void MyMoneyQifReader::Private::finishStatement() { // in case we have collected any data in the statement, we keep it if ((st.m_listTransactions.count() + st.m_listPrices.count() + st.m_listSecurities.count()) > 0) { statements += st; qDebug("Statement with %d transactions, %d prices and %d securities added to the statement list", st.m_listTransactions.count(), st.m_listPrices.count(), st.m_listSecurities.count()); } eMyMoney::Statement::Type type = st.m_eType; //stash type and... // start with a fresh statement st = MyMoneyStatement(); st.m_skipCategoryMatching = !mapCategories; st.m_eType = type; } const QString MyMoneyQifReader::Private::accountTypeToQif(eMyMoney::Account::Type type) const { QString rc = "Bank"; switch (type) { default: break; case eMyMoney::Account::Type::Cash: rc = "Cash"; break; case eMyMoney::Account::Type::CreditCard: rc = "CCard"; break; case eMyMoney::Account::Type::Asset: rc = "Oth A"; break; case eMyMoney::Account::Type::Liability: rc = "Oth L"; break; case eMyMoney::Account::Type::Investment: rc = "Port"; break; } return rc; } const QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const { if (type == "reinvint") return i18nc("Category name", "Reinvested interest"); if (type == "reinvdiv") return i18nc("Category name", "Reinvested dividend"); if (type == "reinvlg") return i18nc("Category name", "Reinvested dividend (long term)"); if (type == "reinvsh") return i18nc("Category name", "Reinvested dividend (short term)"); if (type == "div") return i18nc("Category name", "Dividend"); if (type == "intinc") return i18nc("Category name", "Interest"); if (type == "cgshort") return i18nc("Category name", "Capital Gain (short term)"); if (type == "cgmid") return i18nc("Category name", "Capital Gain (mid term)"); if (type == "cglong") return i18nc("Category name", "Capital Gain (long term)"); if (type == "rtrncap") return i18nc("Category name", "Returned capital"); if (type == "miscinc") return i18nc("Category name", "Miscellaneous income"); if (type == "miscexp") return i18nc("Category name", "Miscellaneous expense"); if (type == "sell" || type == "buy") return i18nc("Category name", "Investment fees"); return i18n("Unknown QIF type %1", type); } bool MyMoneyQifReader::Private::isTransfer(QString& tmp, const QString& leftDelim, const QString& rightDelim) { // it's a transfer, extract the account name // I've seen entries like this // // S[Mehrwertsteuer]/_VATCode_N_I (The '/' is the Quicken class symbol) // // so extracting is a bit more complex and we use a regexp for it QRegExp exp(QString("\\%1(.*)\\%2(.*)").arg(leftDelim, rightDelim)); bool rc; if ((rc = (exp.indexIn(tmp) != -1)) == true) { tmp = exp.cap(1) + exp.cap(2); tmp = tmp.trimmed(); } return rc; } eMyMoney::Split::State MyMoneyQifReader::Private::reconcileState(const QString& state) const { if (state == "X" || state == "R") // Reconciled return eMyMoney::Split::State::Reconciled; if (state == "*") // Cleared return eMyMoney::Split::State::Cleared; return eMyMoney::Split::State::NotReconciled; } MyMoneyQifReader::MyMoneyQifReader() : d(new Private), m_file(nullptr), m_extractedLine(0), m_autoCreatePayee(true), m_pos(0), m_linenumber(0), m_ft(nullptr) { m_skipAccount = false; m_transactionsProcessed = m_transactionsSkipped = 0; m_progressCallback = 0; m_file = 0; m_entryType = EntryUnknown; m_processingData = false; m_userAbort = false; m_warnedInvestment = false; m_warnedSecurity = false; m_warnedPrice = false; connect(&m_filter, SIGNAL(bytesWritten(qint64)), this, SLOT(slotSendDataToFilter())); connect(&m_filter, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter())); connect(&m_filter, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotImportFinished())); connect(&m_filter, SIGNAL(readyReadStandardError()), this, SLOT(slotReceivedErrorFromFilter())); } MyMoneyQifReader::~MyMoneyQifReader() { delete m_file; delete d; } void MyMoneyQifReader::setCategoryMapping(bool map) { d->mapCategories = map; } void MyMoneyQifReader::setURL(const QUrl &url) { m_url = url; } void MyMoneyQifReader::setProfile(const QString& profile) { m_qifProfile.loadProfile("Profile-" + profile); } void MyMoneyQifReader::slotSendDataToFilter() { long len; if (m_file->atEnd()) { m_filter.closeWriteChannel(); } else { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); m_filter.closeWriteChannel(); m_filter.kill(); } else { m_filter.write(m_buffer, len); } } } void MyMoneyQifReader::slotReceivedErrorFromFilter() { qWarning("%s", qPrintable(QString(m_filter.readAllStandardError()))); } void MyMoneyQifReader::slotReceivedDataFromFilter() { parseReceivedData(m_filter.readAllStandardOutput()); } void MyMoneyQifReader::parseReceivedData(const QByteArray& data) { const char* buff = data.data(); int len = data.length(); m_pos += len; // signalProgress(m_pos, 0); while (len) { // process char if (*buff == '\n' || *buff == '\r') { // found EOL if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } m_lineBuffer = QByteArray(); } else { // collect all others m_lineBuffer += (*buff); } ++buff; --len; } } void MyMoneyQifReader::slotImportFinished() { // check if the last EOL char was missing and add the trailing line if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } qDebug("Read %ld bytes", m_pos); QTimer::singleShot(0, this, SLOT(slotProcessData())); } void MyMoneyQifReader::slotProcessData() { signalProgress(-1, -1); // scan the file and try to determine numeric and date formats m_qifProfile.autoDetect(m_qifLines); // the detection is accurate for numeric values, but it could be // that the dates were too ambiguous so that we have to let the user // decide which one to pick. QStringList dateFormats; m_qifProfile.possibleDateFormats(dateFormats); QString format; if (dateFormats.count() > 1) { bool ok; format = QInputDialog::getItem(0, i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, 05, false, &ok); if (!ok) { m_userAbort = true; } } else format = dateFormats.first(); if (!format.isEmpty()) { m_qifProfile.setInputDateFormat(format); qDebug("Selected date format: '%s'", qPrintable(format)); } else { // cancel the process because there is probably nothing to work with m_userAbort = true; } signalProgress(0, m_qifLines.count(), i18n("Importing QIF...")); QStringList::iterator it; for (it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) { ++m_linenumber; // qDebug("Proc: '%s'", (*it).data()); if ((*it).startsWith('!')) { processQifSpecial(*it); m_qifEntry.clear(); } else if (*it == "^") { if (m_qifEntry.count() > 0) { signalProgress(m_linenumber, 0); processQifEntry(); m_qifEntry.clear(); } } else { m_qifEntry += *it; } } d->finishStatement(); qDebug("%d lines processed", m_linenumber); signalProgress(-1, -1); emit statementsReady(d->statements); } bool MyMoneyQifReader::startImport() { bool rc = false; d->st = MyMoneyStatement(); d->st.m_skipCategoryMatching = !d->mapCategories; m_dontAskAgain.clear(); m_accountTranslation.clear(); m_userAbort = false; m_pos = 0; m_linenumber = 0; m_filename.clear(); m_data.clear(); if (m_url.isEmpty()) { return rc; } else if (m_url.isLocalFile()) { m_filename = m_url.toLocalFile(); } else { m_filename = QDir::tempPath(); if(!m_filename.endsWith(QDir::separator())) m_filename += QDir::separator(); m_filename += m_url.fileName(); qDebug() << "Source:" << m_url.toDisplayString() << "Destination:" << m_filename; KIO::FileCopyJob *job = KIO::file_copy(m_url, QUrl::fromUserInput(m_filename), -1, KIO::Overwrite); // KJobWidgets::setWindow(job, kmymoney); if (job->exec() && job->error()) { KMessageBox::detailedError(0, i18n("Error while loading file '%1'.", m_url.toDisplayString()), job->errorString(), i18n("File access error")); return rc; } } m_file = new QFile(m_filename); if (m_file->open(QIODevice::ReadOnly)) { #ifdef DEBUG_IMPORT qint64 len; while (!m_file->atEnd()) { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); } else { parseReceivedData(QByteArray(m_buffer, len)); } } QTimer::singleShot(0, this, SLOT(slotImportFinished())); rc = true; #else QString program; QStringList arguments; program.clear(); arguments.clear(); // start filter process, use 'cat -' as the default filter if (m_qifProfile.filterScriptImport().isEmpty()) { #ifdef Q_OS_WIN32 //krazy:exclude=cpp // this is the Windows equivalent of 'cat -' but since 'type' does not work with stdin // we pass the filename converted to native separators as a parameter program = "cmd.exe"; arguments << "/c"; arguments << "type"; arguments << QDir::toNativeSeparators(m_filename); #else program = "cat"; arguments << "-"; #endif } else { arguments << m_qifProfile.filterScriptImport().split(' ', QString::KeepEmptyParts); program = arguments.takeFirst(); } m_entryType = EntryUnknown; m_filter.setProcessChannelMode(QProcess::MergedChannels); m_filter.start(program, arguments); if (m_filter.waitForStarted()) { signalProgress(0, m_file->size(), i18n("Reading QIF...")); slotSendDataToFilter(); rc = true; // emit statementsReady(d->statements); } else { KMessageBox::detailedError(0, i18n("Error while running the filter '%1'.", m_filter.program()), m_filter.errorString(), i18n("Filter error")); } #endif } return rc; } void MyMoneyQifReader::processQifSpecial(const QString& _line) { QString line = _line.mid(1); // get rid of exclamation mark if (line.left(5).toLower() == QString("type:")) { line = line.mid(5); // exportable accounts if (line.toLower() == "ccard" || KMyMoneySettings::qifCreditCard().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::CreditCard; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "bank" || KMyMoneySettings::qifBank().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Checkings; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "cash" || KMyMoneySettings::qifCash().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Cash; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth a" || KMyMoneySettings::qifAsset().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Asset; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth l" || line.toLower() == i18nc("QIF tag for liability account", "Oth L").toLower()) { d->accountType = eMyMoney::Account::Type::Liability; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "invst" || line.toLower() == i18nc("QIF tag for investment account", "Invst").toLower()) { d->accountType = eMyMoney::Account::Type::Investment; d->transactionType = m_entryType = EntryInvestmentTransaction; } else if (line.toLower() == "invoice" || KMyMoneySettings::qifInvoice().toLower().contains(line.toLower())) { m_entryType = EntrySkip; } else if (line.toLower() == "tax") { m_entryType = EntrySkip; } else if (line.toLower() == "bill") { m_entryType = EntrySkip; // exportable lists } else if (line.toLower() == "cat" || line.toLower() == i18nc("QIF tag for category", "Cat").toLower()) { m_entryType = EntryCategory; } else if (line.toLower() == "security" || line.toLower() == i18nc("QIF tag for security", "Security").toLower()) { m_entryType = EntrySecurity; } else if (line.toLower() == "prices" || line.toLower() == i18nc("QIF tag for prices", "Prices").toLower()) { m_entryType = EntryPrice; } else if (line.toLower() == "payee") { m_entryType = EntryPayee; } else if (line.toLower() == "memorized") { m_entryType = EntryMemorizedTransaction; } else if (line.toLower() == "class" || line.toLower() == i18nc("QIF tag for a class", "Class").toLower()) { m_entryType = EntryClass; } else if (line.toLower() == "budget") { m_entryType = EntrySkip; } else if (line.toLower() == "invitem") { m_entryType = EntrySkip; } else if (line.toLower() == "template") { m_entryType = EntrySkip; } else { qWarning("Unknown type code '%s' in QIF file on line %d", qPrintable(line), m_linenumber); m_entryType = EntrySkip; } // option headers } else if (line.toLower() == "account") { m_entryType = EntryAccount; } else if (line.toLower() == "option:autoswitch") { m_entryType = EntryAccount; } else if (line.toLower() == "clear:autoswitch") { m_entryType = d->transactionType; } } void MyMoneyQifReader::processQifEntry() { // This method processes a 'QIF Entry' which is everything between two caret // signs // try { switch (m_entryType) { case EntryCategory: processCategoryEntry(); break; case EntryUnknown: qDebug() << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed."; processTransactionEntry(); break; case EntryTransaction: processTransactionEntry(); break; case EntryInvestmentTransaction: processInvestmentTransactionEntry(); break; case EntryAccount: processAccountEntry(); break; case EntrySecurity: processSecurityEntry(); break; case EntryPrice: processPriceEntry(); break; case EntryPayee: processPayeeEntry(); break; case EntryClass: qDebug() << "Line " << m_linenumber << ": Classes are not yet supported!"; break; case EntryMemorizedTransaction: qDebug() << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!"; break; case EntrySkip: break; default: qDebug() << "Line " << m_linenumber << ": EntryType " << m_entryType << " not yet implemented!"; break; } } catch (const MyMoneyException &e) { if (QString::fromLatin1(e.what()).contains("USERABORT")) { qDebug() << "Line " << m_linenumber << ": Unhandled error: " << e.what(); } else { m_userAbort = true; } } } const QString MyMoneyQifReader::extractLine(const QChar& id, int cnt) { QStringList::ConstIterator it; m_extractedLine = -1; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { ++m_extractedLine; if ((*it)[0] == id) { if (cnt-- == 1) { return (*it).mid(1); } } } m_extractedLine = -1; return QString(); } bool MyMoneyQifReader::extractSplits(QList& listqSplits) const { // *** With apologies to QString MyMoneyQifReader::extractLine *** QStringList::ConstIterator it; bool ret = false; bool memoPresent = false; int neededCount = 0; qSplit q; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { if (((*it)[0] == 'S') || ((*it)[0] == '$') || ((*it)[0] == 'E')) { memoPresent = false; // in case no memo line in this split if ((*it)[0] == 'E') { q.m_strMemo = (*it).mid(1); // 'E' = Memo d->fixMultiLineMemo(q.m_strMemo); memoPresent = true; // This transaction contains memo } else if ((*it)[0] == 'S') { q.m_strCategoryName = (*it).mid(1); // 'S' = CategoryName neededCount ++; } else if ((*it)[0] == '$') { q.m_amount = (*it).mid(1); // '$' = Amount neededCount ++; } if (neededCount > 1) { // CategoryName & Amount essential listqSplits += q; // Add valid split if (!memoPresent) { // If no memo, clear previous q.m_strMemo.clear(); } q = qSplit(); // Start new split neededCount = 0; ret = true; } } } return ret; } #if 0 void MyMoneyQifReader::processMSAccountEntry(const eMyMoney::Account::Type accountType) { if (extractLine('P').toLower() == m_qifProfile.openingBalanceText().toLower()) { m_account = MyMoneyAccount(); m_account.setAccountType(accountType); QString txt = extractLine('T'); MyMoneyMoney balance = m_qifProfile.value('T', txt); QDate date = m_qifProfile.date(extractLine('D')); m_account.setOpeningDate(date); QString name = extractLine('L'); if (name.left(1) == m_qifProfile.accountDelimiter().left(1)) { name = name.mid(1, name.length() - 2); } d->st_AccountName = name; m_account.setName(name); selectOrCreateAccount(Select, m_account, balance); d->st.m_accountId = m_account.id(); if (! balance.isZero()) { MyMoneyFile* file = MyMoneyFile::instance(); QString openingtxid = file->openingBalanceTransaction(m_account); MyMoneyFileTransaction ft; if (! openingtxid.isEmpty()) { MyMoneyTransaction openingtx = file->transaction(openingtxid); MyMoneySplit split = openingtx.splitByAccount(m_account.id()); if (split.shares() != balance) { const MyMoneySecurity& sec = file->security(m_account.currencyId()); if (KMessageBox::questionYesNo( KMyMoneyUtils::mainWindow(), i18n("The %1 account currently has an opening balance of %2. This QIF file reports an opening balance of %3. Would you like to overwrite the current balance with the one from the QIF file?", m_account.name(), split.shares().formatMoney(m_account, sec), balance.formatMoney(m_account, sec)), i18n("Overwrite opening balance"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "OverwriteOpeningBalance") == KMessageBox::Yes) { file->removeTransaction(openingtx); m_account.setOpeningDate(date); file->createOpeningBalanceTransaction(m_account, balance); } } } else { // Add an opening balance m_account.setOpeningDate(date); file->createOpeningBalanceTransaction(m_account, balance); } ft.commit(); } } else { // for some unknown reason, Quicken 2001 generates the following (somewhat // misleading) sequence of lines: // // 1: !Account // 2: NAT&T Universal // 3: DAT&T Univers(...xxxx) [CLOSED] // 4: TCCard // 5: ^ // 6: !Type:CCard // 7: !Account // 8: NCFCU Visa // 9: DRick's CFCU Visa card (...xxxx) // 10: TCCard // 11: ^ // 12: !Type:CCard // 13: D1/ 4' 1 // // Lines 1-5 are processed via processQifEntry() and processAccountEntry() // Then Quicken issues line 6 but since the account does not carry any // transaction does not write an end delimiter. Arrrgh! So we end up with // a QIF entry comprising of lines 6-11 and end up in this routine. Actually, // lines 7-11 are the leadin for the next account. So we check here if // the !Type:xxx record also contains an !Account line and process the // entry as required. // // (Ace) I think a better solution here is to handle exclamation point // lines separately from entries. In the above case: // Line 1 would set the mode to "account entries". // Lines 2-5 would be interpreted as an account entry. This would set m_account. // Line 6 would set the mode to "cc transaction entries". // Line 7 would immediately set the mode to "account entries" again // Lines 8-11 would be interpreted as an account entry. This would set m_account. // Line 12 would set the mode to "cc transaction entries" // Lines 13+ would be interpreted as cc transaction entries, and life is good int exclamationCnt = 1; QString category; do { category = extractLine('!', exclamationCnt++); } while (!category.isEmpty() && category != "Account"); // we have such a weird empty account if (category == "Account") { processAccountEntry(); } else { selectOrCreateAccount(Select, m_account); d->st_AccountName = m_account.name(); d->st.m_strAccountName = m_account.name(); d->st.m_accountId = m_account.id(); d->st.m_strAccountNumber = m_account.id(); m_account.setNumber(m_account.id()); if (m_entryType == EntryInvestmentTransaction) processInvestmentTransactionEntry(); else processTransactionEntry(); } } } #endif void MyMoneyQifReader::processPayeeEntry() { // TODO } void MyMoneyQifReader::processCategoryEntry() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account = MyMoneyAccount(); account.setName(extractLine('N')); account.setDescription(extractLine('D')); MyMoneyAccount parentAccount; //The extractline routine will more than likely return 'empty', // so also have to test that either the 'I' or 'E' was detected //and set up accounts accordingly. if ((!extractLine('I').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(eMyMoney::Account::Type::Income); parentAccount = file->income(); } else if ((!extractLine('E').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(eMyMoney::Account::Type::Expense); parentAccount = file->expense(); } // check if we can find the account already in the file auto acc = findAccount(account, MyMoneyAccount()); // if not, we just create it if (acc.id().isEmpty()) { MyMoneyAccount brokerage; file->createAccount(account, parentAccount, brokerage, MyMoneyMoney()); } } MyMoneyAccount MyMoneyQifReader::findAccount(const MyMoneyAccount& acc, const MyMoneyAccount& parent) const { static MyMoneyAccount nullAccount; MyMoneyFile* file = MyMoneyFile::instance(); QList parents; try { // search by id if (!acc.id().isEmpty()) { return file->account(acc.id()); } // collect the parents. in case parent does not have an id, we scan the all top-level accounts if (parent.id().isEmpty()) { parents << file->asset(); parents << file->liability(); parents << file->income(); parents << file->expense(); parents << file->equity(); } else { parents << parent; } QList::const_iterator it_p; for (it_p = parents.constBegin(); it_p != parents.constEnd(); ++it_p) { MyMoneyAccount parentAccount = *it_p; // search by name (allow hierarchy) int pos; // check for ':' in the name and use it as separator for a hierarchy QString name = acc.name(); bool notFound = false; while ((pos = name.indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = name.left(pos); QString remainder = name.mid(pos + 1); const auto existingAccount = file->subAccountByName(parentAccount, part); // if account has not been found, continue with next top level parent if (existingAccount.id().isEmpty()) { notFound = true; break; } parentAccount = existingAccount; name = remainder; } if (notFound) continue; const auto existingAccount = file->subAccountByName(parentAccount, name); if (!existingAccount.id().isEmpty()) { if (acc.accountType() != eMyMoney::Account::Type::Unknown) { if (acc.accountType() != existingAccount.accountType()) continue; } return existingAccount; } } } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Unable to find account: %1", QString::fromLatin1(e.what()))); } return nullAccount; } const QString MyMoneyQifReader::transferAccount(const QString& name, bool useBrokerage) { QString accountId; QStringList tmpEntry = m_qifEntry; // keep temp copies MyMoneyAccount tmpAccount = m_account; m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); // in case we found a reference to an investment account, we need // to switch to the brokerage account instead. MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); if (useBrokerage && (acc.accountType() == eMyMoney::Account::Type::Investment)) { m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(acc.brokerageName()); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); } m_qifEntry = tmpEntry; // restore local copies m_account = tmpAccount; return accountId; } void MyMoneyQifReader::createOpeningBalance(eMyMoney::Account::Type accType) { MyMoneyFile* file = MyMoneyFile::instance(); // if we don't have a name for the current account we need to extract the name from the L-record if (m_account.name().isEmpty()) { QString name = extractLine('L'); if (name.isEmpty()) { name = i18n("QIF imported, no account name supplied"); } auto b = d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)); Q_UNUSED(b) QStringList entry = m_qifEntry; // keep a temp copy m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType)); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); processAccountEntry(); m_qifEntry = entry; // restore local copy } MyMoneyFileTransaction ft; try { bool needCreate = true; MyMoneyAccount acc = m_account; // in case we're dealing with an investment account, we better use // the accompanying brokerage account for the opening balance acc = file->accountByName(m_account.brokerageName()); // check if we already have an opening balance transaction QString tid = file->openingBalanceTransaction(acc); MyMoneyTransaction ot; if (!tid.isEmpty()) { ot = file->transaction(tid); MyMoneySplit s0 = ot.splitByAccount(acc.id()); // if the value is the same, we can silently skip this transaction if (s0.shares() == m_qifProfile.value('T', extractLine('T'))) { needCreate = false; } if (needCreate) { // in case we create it anyway, we issue a warning to the user to check it manually KMessageBox::sorry(0, QString("%1").arg(i18n("KMyMoney has imported a second opening balance transaction into account %1 which differs from the one found already on file. Please correct this manually once the import is done.", acc.name())), i18n("Opening balance problem")); } } if (needCreate) { acc.setOpeningDate(m_qifProfile.date(extractLine('D'))); file->modifyAccount(acc); MyMoneyTransaction t = file->createOpeningBalanceTransaction(acc, m_qifProfile.value('T', extractLine('T'))); if (!t.id().isEmpty()) { t.setImported(); file->modifyTransaction(t); } ft.commit(); } // make sure to use the updated version of the account if (m_account.id() == acc.id()) m_account = acc; // remember which account we created d->st.m_accountId = m_account.id(); } catch (const MyMoneyException &e) { KMessageBox::detailedError(nullptr, i18n("Error while creating opening balance transaction"), e.what(), i18n("File access error")); } } void MyMoneyQifReader::processTransactionEntry() { ++m_transactionsProcessed; // in case the user selected to skip the account or the account // was not found we skip this transaction /* if(m_account.id().isEmpty()) { m_transactionsSkipped++; return; } */ MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyStatement::Split s1; MyMoneyStatement::Transaction tr; QString tmp; QString accountId; int pos; QString payee = extractLine('P'); unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; if (d->firstTransaction) { // check if this is an opening balance transaction and process it out of the statement if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneySettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(d->accountType); d->firstTransaction = false; return; } } // Process general transaction data if (d->st.m_accountId.isEmpty()) d->st.m_accountId = m_account.id(); s1.m_accountId = d->st.m_accountId; switch (d->accountType) { case eMyMoney::Account::Type::Checkings: d->st.m_eType=eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: d->st.m_eType=eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: d->st.m_eType=eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: d->st.m_eType=eMyMoney::Statement::Type::CreditCard; break; default: d->st.m_eType=eMyMoney::Statement::Type::None; break; } tr.m_datePosted = (m_qifProfile.date(extractLine('D'))); if (!tr.m_datePosted.isValid()) { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = (QDate::currentDate()); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION_CSTRING("USERABORT"); break; } } tmp = extractLine('L'); pos = tmp.lastIndexOf("--"); if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) { // it's a transfer, so we wipe the memo // tmp = ""; why?? // st.m_strAccountName = tmp; } else if (pos != -1) { // what's this? // t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); } // t.setMemo(tmp); // Assign the "#" field to the transaction's bank id // This is the custom KMM extension to QIF for a unique ID tmp = extractLine('#'); if (!tmp.isEmpty()) { tr.m_strBankID = QString("ID %1").arg(tmp); } #if 0 // Collect data for the account's split s1.m_accountId = m_account.id(); tmp = extractLine('S'); pos = tmp.findRev("--"); if (pos != -1) { tmp = tmp.left(pos); } if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) // it's a transfer, extract the account name tmp = tmp.mid(1, tmp.length() - 2); s1.m_strCategoryName = tmp; #endif // TODO (Ace) Deal with currencies more gracefully. QIF cannot deal with multiple // currencies, so we should assume that transactions imported into a given // account are in THAT ACCOUNT's currency. If one of those involves a transfer // to an account with a different currency, value and shares should be // different. (Shares is in the target account's currency, value is in the // transaction's) s1.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_shares = m_qifProfile.value('T', extractLine('T')); tmp = extractLine('N'); if (!tmp.isEmpty()) tr.m_strNumber = tmp; if (!payee.isEmpty()) { tr.m_strPayee = payee; } tr.m_reconcile = d->reconcileState(extractLine('C')); tr.m_strMemo = extractLine('M'); d->fixMultiLineMemo(tr.m_strMemo); s1.m_strMemo = tr.m_strMemo; // tr.m_listSplits.append(s1); // split transaction // ****** ensure each field is ****** // * attached to correct split * QList listqSplits; if (! extractSplits(listqSplits)) { MyMoneyAccount account; // use the same values for the second split, but clear the ID and reverse the value MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = (-s1.m_amount); // s2.clearId(); // standard transaction tmp = extractLine('L'); if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { /* pos = tmp.findRev("--"); if(pos != -1) { t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); }*/ // it's an expense / income tmp = tmp.trimmed(); accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount); } if (!accountId.isEmpty()) { try { account = file->account(accountId); // FIXME: check that the type matches and ask if not if (account.accountType() == eMyMoney::Account::Type::Investment) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored."; return; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits.append(s2); } } else { int count; for (count = 1; count <= listqSplits.count(); ++count) { // Use true splits count MyMoneyStatement::Split s2 = s1; s2.m_amount = (-m_qifProfile.value('$', listqSplits[count-1].m_amount)); // Amount of split s2.m_strMemo = listqSplits[count-1].m_strMemo; // Memo in split tmp = listqSplits[count-1].m_strCategoryName; // Category in split if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { pos = tmp.lastIndexOf("--"); if (pos != -1) { tmp = tmp.left(pos); } tmp = tmp.trimmed(); accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount); } if (!accountId.isEmpty()) { try { MyMoneyAccount account = file->account(accountId); // FIXME: check that the type matches and ask if not if (account.accountType() == eMyMoney::Account::Type::Investment) { qDebug() << "Line " << m_linenumber << ": Cannot convert a split transfer to/from an investment account. Split removed. Total amount adjusted from " << tr.m_amount.formatMoney("", 2) << " to " << (tr.m_amount + s2.m_amount).formatMoney("", 2) << "\n"; tr.m_amount += s2.m_amount; continue; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits += s2; // in case the transaction does not have a memo and we // process the first split just copy the memo over if (tr.m_listSplits.count() == 1 && tr.m_strMemo.isEmpty()) tr.m_strMemo = s2.m_strMemo; } else { // TODO add an option to create a "Unassigned" category // for now, we just drop the split which will show up as unbalanced // transaction in the KMyMoney ledger view } } } // Add the transaction to the statement d->st.m_listTransactions += tr; } void MyMoneyQifReader::processInvestmentTransactionEntry() { // qDebug() << "Investment Transaction:" << m_qifEntry.count() << " lines"; /* Items for Investment Accounts Field Indicator Explanation D Date N Action Y Security (NAME, not symbol) I Price Q Quantity (number of shares or split ratio) T Transaction amount C Cleared status P Text in the first line for transfers and reminders (Payee) M Memo O Commission L Account for the transfer $ Amount transferred ^ End of the entry It will be presumed all transactions are to the associated cash account, if one exists, unless otherwise noted by the 'L' field. Expense/Income categories will be automatically generated, "_Dividend", "_InterestIncome", etc. */ MyMoneyStatement::Transaction tr; d->st.m_eType = eMyMoney::Statement::Type::Investment; // t.setCommodity(m_account.currencyId()); // 'D' field: Date QDate date = m_qifProfile.date(extractLine('D')); if (date.isValid()) tr.m_datePosted = date; else { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = QDate::currentDate(); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION_CSTRING("USERABORT"); break; } } // 'M' field: Memo QString memo = extractLine('M'); d->fixMultiLineMemo(memo); tr.m_strMemo = memo; unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; // '#' field: BankID QString tmp = extractLine('#'); if (! tmp.isEmpty()) tr.m_strBankID = QString("ID %1").arg(tmp); // Reconciliation flag tr.m_reconcile = d->reconcileState(extractLine('C')); // 'O' field: Fees tr.m_fees = m_qifProfile.value('T', extractLine('O')); // 'T' field: Amount MyMoneyMoney amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = amount; MyMoneyStatement::Price price; price.m_date = date; price.m_strSecurity = extractLine('Y'); price.m_amount = m_qifProfile.value('T', extractLine('I')); #if 0 // we must check for that later, because certain activities don't need a security // 'Y' field: Security name QString securityname = extractLine('Y').toLower(); if (securityname.isEmpty()) { qDebug() << "Line " << m_linenumber << ": Investment transaction without a security is not supported."; return; } tr.m_strSecurity = securityname; #endif #if 0 // For now, we let the statement reader take care of that. // The big problem here is that the Y field is not the SYMBOL, it's the NAME. // The name is not very unique, because people could have used slightly different // abbreviations or ordered words differently, etc. // // If there is a perfect name match with a subordinate stock account, great. // More likely, we have to rely on the QIF file containing !Type:Security // records, which tell us the mapping from name to symbol. // // Therefore, generally it is not recommended to import a QIF file containing // investment transactions but NOT containing security records. QString securitysymbol = m_investmentMap[securityname]; // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and either // (2a) the security name of the transaction matches the name of the security, OR // (2b) the security name of the transaction maps to a symbol which matches the symbol of the security // search through each subordinate account bool found = false; MyMoneyAccount thisaccount = m_account; QStringList accounts = thisaccount.accountList(); QStringList::const_iterator it_account = accounts.begin(); while (!found && it_account != accounts.end()) { QString currencyid = file->account(*it_account).currencyId(); MyMoneySecurity security = file->security(currencyid); QString symbol = security.tradingSymbol().toLower(); QString name = security.name().toLower(); if (securityname == name || securitysymbol == symbol) { d->st_AccountId = *it_account; s1.m_accountId = *it_account; thisaccount = file->account(*it_account); found = true; #if 0 // update the price, while we're here. in the future, this should be // an option QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(currencyid, basecurrencyid, t_in.m_datePosted, true); if (!price.isValid()) { MyMoneyPrice newprice(currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_moneyAmount / t_in.m_dShares, i18n("Statement Importer")); file->addPrice(newprice); } #endif } ++it_account; } if (!found) { qDebug() << "Line " << m_linenumber << ": Security " << securityname << " not found in this account. Transaction ignored."; // If the security is not known, notify the user // TODO (Ace) A "SelectOrCreateAccount" interface for investments KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. " "Transactions involving this security will be ignored.", securityname), i18n("Security not found"), QString("MissingSecurity%1").arg(securityname.trimmed())); return; } #endif // 'Y' field: Security tr.m_strSecurity = extractLine('Y'); // 'Q' field: Quantity MyMoneyMoney quantity = m_qifProfile.value('T', extractLine('Q')); // 'N' field: Action QString action = extractLine('N').toLower(); // remove trailing X, which seems to have no purpose (?!) bool xAction = false; if (action.endsWith('x')) { action = action.left(action.length() - 1); xAction = true; } tmp = extractLine('L'); // if the action ends in an X, the L-Record contains the asset account // to which the dividend should be transferred. In the other cases, it // may contain a category that identifies the income category for the // dividend payment if ((xAction == true) || (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' if (!tmp.isEmpty()) { // use 'L' record name tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } else { tr.m_strBrokerageAccount = m_account.brokerageName();// use brokerage account transferAccount(m_account.brokerageName()); // make sure the account exists } } else { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' tr.m_strInterestCategory = tmp; tr.m_strBrokerageAccount = m_account.brokerageName(); } // Whether to create a cash split for the other side of the value QString accountname; //= extractLine('L'); if (action == "reinvint" || action == "reinvdiv" || action == "reinvlg" || action == "reinvsh") { d->st.m_listPrices += price; tr.m_shares = quantity; tr.m_eAction = (eMyMoney::Transaction::Action::ReinvestDividend); tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_strInterestCategory = extractLine('L'); if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } } else if (action == "div" || action == "cgshort" || action == "cgmid" || action == "cglong" || action == "rtrncap") { tr.m_eAction = (eMyMoney::Transaction::Action::CashDividend); // make sure, we have valid category. Either taken from the L-Record above, // or derived from the action code if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. tr.m_amount -= tr.m_fees; // We need an extra split which will be the zero-amount investment split // that serves to mark this transaction as a cash dividend and note which // stock account it belongs to. MyMoneyStatement::Split s2; s2.m_amount = MyMoneyMoney(); s2.m_strCategoryName = extractLine('Y'); tr.m_listSplits.append(s2); } else if (action == "intinc" || action == "miscinc" || action == "miscexp") { tr.m_eAction = (eMyMoney::Transaction::Action::Interest); if (action == "miscexp") tr.m_eAction = (eMyMoney::Transaction::Action::Fees); // make sure, we have a valid category. Either taken from the L-Record above, // or derived from the action code if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } if (action == "intinc") { MyMoneyMoney priceValue = m_qifProfile.value('I', extractLine('I')); tr.m_amount -= tr.m_fees; if ((!quantity.isZero()) && (!priceValue.isZero())) tr.m_amount = -(quantity * priceValue); } else // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. if (action != "miscexp") tr.m_amount = -(amount - tr.m_fees); if (tr.m_strMemo.isEmpty()) tr.m_strMemo = (QString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).trimmed(); } else if (action == "xin" || action == "xout") { QString payee = extractLine('P'); if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneySettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(eMyMoney::Account::Type::Investment); return; } tr.m_eAction = (eMyMoney::Transaction::Action::None); MyMoneyStatement::Split s2; tmp = extractLine('L'); if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { s2.m_accountId = transferAccount(tmp); s2.m_strCategoryName = tmp; } else { s2.m_strCategoryName = extractLine('L'); if (tr.m_strInterestCategory.isEmpty()) { s2.m_strCategoryName = d->typeToAccountName(action); } } if (action == "xout") tr.m_amount = -tr.m_amount; s2.m_amount = -tr.m_amount; tr.m_listSplits.append(s2); } else if (action == "buy") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = quantity; tr.m_amount = -amount; tr.m_eAction = (eMyMoney::Transaction::Action::Buy); } else if (action == "sell") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = -quantity; tr.m_amount = amount; tr.m_eAction = (eMyMoney::Transaction::Action::Sell); } else if (action == "shrsin") { tr.m_shares = quantity; tr.m_eAction = (eMyMoney::Transaction::Action::Shrsin); } else if (action == "shrsout") { tr.m_shares = -quantity; tr.m_eAction = (eMyMoney::Transaction::Action::Shrsout); } else if (action == "stksplit") { MyMoneyMoney splitfactor = (quantity / MyMoneyMoney(10, 1)).reduce(); // Stock splits not supported // qDebug() << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")"; // s1.setShares(splitfactor); // s1.setValue(0); // s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)); // return; } else { // Unsupported action type qDebug() << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")"; return; } d->st.m_strAccountName = accountname; // accountname appears not to get set d->st.m_listTransactions += tr; /************************************************************************* * * These transactions are natively supported by KMyMoney * *************************************************************************/ /* D1/ 3' 5 NShrsIn YGENERAL MOTORS CORP 52BR1 I20 Q200 U4,000.00 T4,000.00 M200 shares added to account @ $20/share ^ */ /* ^ D1/14' 5 NShrsOut YTEMPLETON GROWTH 97GJ0 Q50 90 ^ */ /* D1/28' 5 NBuy YGENERAL MOTORS CORP 52BR1 I24.35 Q100 U2,435.00 T2,435.00 ^ */ /* D1/ 5' 5 NSell YUnited Vanguard I8.41 Q50 U420.50 T420.50 ^ */ /* D1/ 7' 5 NReinvDiv YFRANKLIN INCOME 97GM2 I38 Q1 U38.00 T38.00 ^ */ /************************************************************************* * * These transactions are all different kinds of income. (Anything that * follows the DNYUT pattern). They are all handled the same, the only * difference is which income account the income is placed into. By * default, it's placed into _xxx where xxx is the right side of the * N field. e.g. NDiv transaction goes into the _Div account * *************************************************************************/ /* D1/10' 5 NDiv YTEMPLETON GROWTH 97GJ0 U10.00 T10.00 ^ */ /* D1/10' 5 NIntInc YTEMPLETON GROWTH 97GJ0 U20.00 T20.00 ^ */ /* D1/10' 5 NCGShort YTEMPLETON GROWTH 97GJ0 U111.00 T111.00 ^ */ /* D1/10' 5 NCGLong YTEMPLETON GROWTH 97GJ0 U333.00 T333.00 ^ */ /* D1/10' 5 NCGMid YTEMPLETON GROWTH 97GJ0 U222.00 T222.00 ^ */ /* D2/ 2' 5 NRtrnCap YFRANKLIN INCOME 97GM2 U1,234.00 T1,234.00 ^ */ /************************************************************************* * * These transactions deal with miscellaneous activity that KMyMoney * does not support, but may support in the future. * *************************************************************************/ /* Note the Q field is the split ratio per 10 shares, so Q12.5 is a 12.5:10 split, otherwise known as 5:4. D1/14' 5 NStkSplit YIBM Q12.5 ^ */ /************************************************************************* * * These transactions deal with short positions and options, which are * not supported at all by KMyMoney. They will be ignored for now. * There may be a way to hack around this, by creating a new security * "IBM_Short". * *************************************************************************/ /* D1/21' 5 NShtSell YIBM I92.38 Q100 U9,238.00 T9,238.00 ^ */ /* D1/28' 5 NCvrShrt YIBM I92.89 Q100 U9,339.00 T9,339.00 O50.00 ^ */ /* D6/ 1' 5 NVest YIBM Option Q20 ^ */ /* D6/ 8' 5 NExercise YIBM Option I60.952381 Q20 MFrom IBM Option Grant 6/1/2004 ^ */ /* D6/ 1'14 NExpire YIBM Option Q5 ^ */ /************************************************************************* * * These transactions do not have an associated investment ("Y" field) * so presumably they are only valid for the cash account. Once I * understand how these are really implemented, they can probably be * handled without much trouble. * *************************************************************************/ /* D1/14' 5 NCash U-100.00 T-100.00 LBank Chrg ^ */ /* D1/15' 5 NXOut U500.00 T500.00 L[CU Savings] $500.00 ^ */ /* D1/28' 5 NXIn U1,000.00 T1,000.00 L[CU Checking] $1,000.00 ^ */ /* D1/25' 5 NMargInt U25.00 T25.00 ^ */ } const QString MyMoneyQifReader::findOrCreateIncomeAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->income(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.constBegin(); while (it_accid != list.constEnd()) { acc = file->account(*it_accid); if (acc.name() == searchname) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if (result.isEmpty()) { MyMoneyAccount newAccount; newAccount.setName(searchname); newAccount.setAccountType(eMyMoney::Account::Type::Income); MyMoneyAccount income = file->income(); MyMoneyFileTransaction ft; file->addAccount(newAccount, income); ft.commit(); result = newAccount.id(); } return result; } // TODO (Ace) Combine this and the previous function const QString MyMoneyQifReader::findOrCreateExpenseAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->expense(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.constBegin(); while (it_accid != list.constEnd()) { acc = file->account(*it_accid); if (acc.name() == searchname) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if (result.isEmpty()) { MyMoneyAccount newAccount; newAccount.setName(searchname); newAccount.setAccountType(eMyMoney::Account::Type::Expense); MyMoneyFileTransaction ft; MyMoneyAccount expense = file->expense(); file->addAccount(newAccount, expense); ft.commit(); result = newAccount.id(); } return result; } const QString MyMoneyQifReader::processAccountEntry(bool resetAccountId) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; QString tmp; account.setName(extractLine('N')); // qDebug("Process account '%s'", account.name().data()); account.setDescription(extractLine('D')); tmp = extractLine('$'); if (tmp.length() > 0) account.setValue("lastStatementBalance", tmp); tmp = extractLine('/'); if (tmp.length() > 0) account.setLastReconciliationDate(m_qifProfile.date(tmp)); QifEntryTypeE transactionType = EntryTransaction; QString type = extractLine('T').toLower().remove(QRegExp("\\s+")); if (type == m_qifProfile.profileType().toLower().remove(QRegExp("\\s+"))) { account.setAccountType(eMyMoney::Account::Type::Checkings); } else if (type == "ccard" || type == "creditcard") { account.setAccountType(eMyMoney::Account::Type::CreditCard); } else if (type == "cash") { account.setAccountType(eMyMoney::Account::Type::Cash); } else if (type == "otha") { account.setAccountType(eMyMoney::Account::Type::Asset); } else if (type == "othl") { account.setAccountType(eMyMoney::Account::Type::Liability); } else if (type == "invst" || type == "port") { account.setAccountType(eMyMoney::Account::Type::Investment); transactionType = EntryInvestmentTransaction; } else if (type == "mutual") { // stock account w/o umbrella investment account account.setAccountType(eMyMoney::Account::Type::Stock); transactionType = EntryInvestmentTransaction; } else if (type == "unknown") { // don't do anything with the type, leave it unknown } else { account.setAccountType(eMyMoney::Account::Type::Checkings); qDebug() << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed"; } // check if we can find the account already in the file auto acc = findAccount(account, MyMoneyAccount()); if (acc.id().isEmpty()) { // in case the account is not found by name and the type is // unknown, we have to assume something and create a checking account. // this might be wrong, but we have no choice at this point. if (account.accountType() == eMyMoney::Account::Type::Unknown) account.setAccountType(eMyMoney::Account::Type::Checkings); MyMoneyAccount parentAccount; MyMoneyAccount brokerage; // in case it's a stock account, we need to setup a fix investment account if (account.isInvest()) { acc.setName(i18n("%1 (Investment)", account.name())); // use the same name for the investment account acc.setDescription(i18n("Autogenerated by QIF importer from type Mutual account entry")); acc.setAccountType(eMyMoney::Account::Type::Investment); parentAccount = file->asset(); file->createAccount(acc, parentAccount, brokerage, MyMoneyMoney()); parentAccount = acc; qDebug("We still need to create the stock account in MyMoneyQifReader::processAccountEntry()"); } else { // setup parent according the type of the account switch (account.accountGroup()) { case eMyMoney::Account::Type::Asset: default: parentAccount = file->asset(); break; case eMyMoney::Account::Type::Liability: parentAccount = file->liability(); break; case eMyMoney::Account::Type::Equity: parentAccount = file->equity(); break; } } // investment accounts will receive a brokerage account, as KMyMoney // currently does not allow to store funds in the investment account directly // but only create it (not here, but later) if it is needed if (account.accountType() == eMyMoney::Account::Type::Investment) { brokerage.setName(QString()); // brokerage name empty so account not created yet brokerage.setAccountType(eMyMoney::Account::Type::Checkings); brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); } file->createAccount(account, parentAccount, brokerage, MyMoneyMoney()); acc = account; // qDebug("Account created"); } else { // qDebug("Existing account found"); } if (resetAccountId) { // possibly start a new statement d->finishStatement(); m_account = acc; d->st.m_accountId = m_account.id(); // needed here for account selection d->transactionType = transactionType; } return acc.id(); } void MyMoneyQifReader::setProgressCallback(void(*callback)(qint64, qint64, const QString&)) { m_progressCallback = callback; } void MyMoneyQifReader::signalProgress(qint64 current, qint64 total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyQifReader::processPriceEntry() { /* !Type:Prices "IBM",141 9/16,"10/23/98" ^ !Type:Prices "GMW",21.28," 3/17' 5" ^ !Type:Prices "GMW",71652181.001,"67/128/ 0" ^ Note that Quicken will often put in a price with a bogus date and number. We will ignore prices with bogus dates. Hopefully that will catch all of these. Also note that prices can be in fractional units, e.g. 141 9/16. */ QStringList::const_iterator it_line = m_qifEntry.constBegin(); // Make a price for each line QRegExp priceExp("\"(.*)\",(.*),\"(.*)\""); while (it_line != m_qifEntry.constEnd()) { if (priceExp.indexIn(*it_line) != -1) { MyMoneyStatement::Price price; price.m_strSecurity = priceExp.cap(1); QString pricestr = priceExp.cap(2); QString datestr = priceExp.cap(3); qDebug() << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr; // Only add the price if the date is valid. If invalid, fail silently. See note above. // Also require the price value to not have any slashes. Old prices will be something like // "25 9/16", which we do not support. So we'll skip the price for now. QDate date = m_qifProfile.date(datestr); MyMoneyMoney rate(m_qifProfile.value('P', pricestr)); if (date.isValid() && !rate.isZero()) { price.m_amount = rate; price.m_date = date; d->st.m_listPrices += price; } } ++it_line; } } void MyMoneyQifReader::processSecurityEntry() { /* !Type:Security NVANGUARD 500 INDEX SVFINX TMutual Fund ^ */ MyMoneyStatement::Security security; security.m_strName = extractLine('N'); security.m_strSymbol = extractLine('S'); d->st.m_listSecurities += security; } + +int MyMoneyQifReader::statementCount() const +{ + return d->statements.count(); +} diff --git a/kmymoney/plugins/qif/import/mymoneyqifreader.h b/kmymoney/plugins/qif/import/mymoneyqifreader.h index 47b956859..26cea52a0 100644 --- a/kmymoney/plugins/qif/import/mymoneyqifreader.h +++ b/kmymoney/plugins/qif/import/mymoneyqifreader.h @@ -1,349 +1,351 @@ /*************************************************************************** mymoneyqifreader.h - description ------------------- begin : Mon Jan 27 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef MYMONEYQIFREADER_H #define MYMONEYQIFREADER_H // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyaccount.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" #include "../config/mymoneyqifprofile.h" class MyMoneyFileTransaction; class MyMoneyStatement; class MyMoneyQifProfile; /** * @author Thomas Baumgart */ class MyMoneyQifReader : public QObject { Q_OBJECT friend class Private; private: typedef enum { EntryUnknown = 0, EntryAccount, EntryTransaction, EntryCategory, EntryMemorizedTransaction, EntryInvestmentTransaction, EntrySecurity, EntryPrice, EntryPayee, EntryClass, EntrySkip } QifEntryTypeE; struct qSplit { QString m_strCategoryName; QString m_strMemo; QString m_amount; }; public: MyMoneyQifReader(); ~MyMoneyQifReader(); /** * This method is used to store the filename into the object. * The file should exist. If it does and an external filter * program is specified with the current selected profile, * the file is send through this filter and the result * is stored in the m_tempFile file. * * @param url URL of the file to be imported */ void setURL(const QUrl &url); /** * This method is used to store the name of the profile into the object. * The selected profile will be loaded if it exists. If an external * filter program is specified with the current selected profile, * the file is send through this filter and the result * is stored in the m_tempFile file. * * @param name QString reference to the name of the profile */ void setProfile(const QString& name); /** * This method actually starts the import of data from the selected file * into the MyMoney engine. * * This method also starts the user defined import filter program * defined in the QIF profile. If none is defined, the file is read * as is (actually the UNIX command 'cat -' is used as the filter). * * If data from the filter program is available, the slot * slotReceivedDataFromFilter() will be called. * * Make sure to connect the signal importFinished() to detect when * the import actually ended. Call the method finishImport() to clean * things up and get the overall result of the import. * * @retval true the import was started successfully * @retval false the import could not be started. */ bool startImport(); void setCategoryMapping(bool map); inline const MyMoneyAccount& account() const { return m_account; }; + int statementCount() const; + void setProgressCallback(void(*callback)(qint64, qint64, const QString&)); private: /** * This method is used to update the progress information. It * checks if an appropriate function is known and calls it. * * For a parameter description see KMyMoneyView::progressCallback(). */ void signalProgress(qint64 current, qint64 total, const QString& = ""); /** * This method scans a transaction contained in * a QIF file formatted as an account record. This * format is used by MS-Money. If the specific data * is not found, then the data in the entry is treated * as a transaction. In this case, the user will be asked to * specify the account to which the transactions should be imported. * The entry data is found in m_qifEntry. * * @param accountType see MyMoneyAccount() for details. Defaults to eMyMoney::Account::Type::Checkings */ void processMSAccountEntry(const eMyMoney::Account::Type accountType = eMyMoney::Account::Type::Checkings); /** * This method scans the m_qifEntry object as a payee record specified by Quicken */ void processPayeeEntry(); /** * This method scans the m_qifEntry object as an account record specified * by Quicken. In case @p resetAccountId is @p true (the default), the * global account id will be reset. * * The id of the account will be returned. */ const QString processAccountEntry(bool resetAccountId = true); /** * This method scans the m_qifEntry object as a category record specified * by Quicken. */ void processCategoryEntry(); /** * This method scans the m_qifEntry object as a transaction record specified * by Quicken. */ void processTransactionEntry(); /** * This method scans the m_qifEntry object as an investment transaction * record specified by Quicken. */ void processInvestmentTransactionEntry(); /** * This method scans the m_qifEntry object as a price record specified * by Quicken. */ void processPriceEntry(); /** * This method scans the m_qifEntry object as a security record specified * by Quicken. */ void processSecurityEntry(); /** * This method processes the lines previously collected in * the member variable m_qifEntry. If further information * by the user is required to process the entry it will * be collected. */ void processQifEntry(); /** * This method process a line starting with an exclamation mark */ void processQifSpecial(const QString& _line); /** * This method extracts the line beginning with the letter @p id * from the lines contained in the QStringList object @p m_qifEntry. * An empty QString is returned, if the line is not found. * * @param id QChar containing the letter to be found * @param cnt return cnt'th of occurrence of id in lines. cnt defaults to 1. * * @return QString with the remainder of the line or empty if * @p id is not found in @p lines */ const QString extractLine(const QChar& id, int cnt = 1); /** * This method examines each line in the QStringList object @p m_qifEntry, * searching for split entries, which it extracts into a struct qSplit and * stores all splits found in @p listqSplits . */ bool extractSplits(QList& listqSplits) const; enum SelectCreateMode { Create = 0, Select }; /** * This method looks up the @p searchname account by name and returns its id * if it was found. If it was not found, it creates a new income account using * @p searchname as a name, and returns the id if the newly created account * * @param searchname The name of the account to find or create * @return QString id of the found or created account */ static const QString findOrCreateIncomeAccount(const QString& searchname); /** * This method looks up the @p searchname account by name and returns its id * if it was found. If it was not found, it creates a new expense account using * @p searchname as a name, and returns the id if the newly created account * * @param searchname The name of the account to find or create * @return QString id of the found or created account */ static const QString findOrCreateExpenseAccount(const QString& searchname); /** * This methods returns the account from the list of accounts identified by * an account id or account name including an account hierachy. * * The parent account specifies from which account the search should be started. * In case the parent account does not have an id, the method scans all top-level accounts. * * If the account is not found in the list of accounts, MyMoneyAccount() is returned. * * @param acc account to find * @param parent parent account to search from * @retval found MyMoneyAccount account instance * @retval MyMoneyAccount() if not found */ MyMoneyAccount findAccount(const MyMoneyAccount& acc, const MyMoneyAccount& parent) const; /** * This method returns the account id for a given account @a name. In * case @a name references an investment account and @a useBrokerage is @a true * (the default), the id of the corresponding brokerage account will be * returned. In case an account does not exist, it will be created. */ const QString transferAccount(const QString& name, bool useBrokerage = true); // void processQifLine(); void createOpeningBalance(eMyMoney::Account::Type accType = eMyMoney::Account::Type::Checkings); Q_SIGNALS: void statementsReady(const QList &); private Q_SLOTS: void slotSendDataToFilter(); void slotReceivedDataFromFilter(); void slotReceivedErrorFromFilter(); void slotProcessData(); /** * This slot is used to be informed about the end of the filtering process. * It emits the signal importFinished() */ void slotImportFinished(); private: void parseReceivedData(const QByteArray& data); /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; QProcess m_filter; QString m_filename; QUrl m_url; MyMoneyQifProfile m_qifProfile; MyMoneyAccount m_account; unsigned long m_transactionsSkipped; unsigned long m_transactionsProcessed; QStringList m_dontAskAgain; QMap m_accountTranslation; QMap m_investmentMap; QFile *m_file; char m_buffer[1024]; QByteArray m_lineBuffer; QStringList m_qifEntry; int m_extractedLine; QString m_qifLine; QStringList m_qifLines; QifEntryTypeE m_entryType; bool m_skipAccount; bool m_processingData; bool m_userAbort; bool m_autoCreatePayee; unsigned long m_pos; unsigned m_linenumber; bool m_warnedInvestment; bool m_warnedSecurity; bool m_warnedPrice; QList m_transactionCache; QList m_data; void (*m_progressCallback)(qint64, qint64, const QString&); MyMoneyFileTransaction* m_ft; }; #endif diff --git a/kmymoney/plugins/qif/import/qifimporter.cpp b/kmymoney/plugins/qif/import/qifimporter.cpp index 11d742929..bbef21357 100644 --- a/kmymoney/plugins/qif/import/qifimporter.cpp +++ b/kmymoney/plugins/qif/import/qifimporter.cpp @@ -1,108 +1,111 @@ /*************************************************************************** qifimporter.cpp ------------------- copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qifimporter.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kimportdlg.h" #include "mymoneyqifreader.h" #include "statementinterface.h" #include "viewinterface.h" class MyMoneyStatement; QIFImporter::QIFImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "qifimporter"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args); setComponentName("qifimporter", i18n("QIF importer")); setXMLFile("qifimporter.rc"); createActions(); // For information, announce that we have been loaded. qDebug("Plugins: qifimporter loaded"); } QIFImporter::~QIFImporter() { qDebug("Plugins: qifimporter unloaded"); } void QIFImporter::createActions() { const auto &kpartgui = QStringLiteral("file_import_qif"); m_action = actionCollection()->addAction(kpartgui); m_action->setText(i18n("QIF...")); connect(m_action, &QAction::triggered, this, &QIFImporter::slotQifImport); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void QIFImporter::slotQifImport() { m_action->setEnabled(false); QPointer dlg = new KImportDlg(nullptr); if (dlg->exec() == QDialog::Accepted && dlg != nullptr) { m_qifReader = new MyMoneyQifReader; + statementInterface()->resetMessages(); connect(m_qifReader, &MyMoneyQifReader::statementsReady, this, &QIFImporter::slotGetStatements); m_qifReader->setURL(dlg->file()); m_qifReader->setProfile(dlg->profile()); m_qifReader->setCategoryMapping(dlg->m_typeComboBox->currentIndex() == 0); + const auto statementCount = m_qifReader->statementCount(); if (!m_qifReader->startImport()) delete m_qifReader; + statementInterface()->showMessages(statementCount); } delete dlg; m_action->setEnabled(true); } bool QIFImporter::slotGetStatements(const QList &statements) { auto ret = true; QStringList importSummary; for (const auto& statement : statements) { const auto singleImportSummary = statementInterface()->import(statement); if (singleImportSummary.isEmpty()) ret = false; importSummary.append(singleImportSummary); } delete m_qifReader; if (!importSummary.isEmpty()) KMessageBox::informationList(nullptr, i18n("The statement has been processed with the following results:"), importSummary, i18n("Statement stats")); return ret; } K_PLUGIN_FACTORY_WITH_JSON(QIFImporterFactory, "qifimporter.json", registerPlugin();) #include "qifimporter.moc" diff --git a/kmymoney/plugins/statementinterface.h b/kmymoney/plugins/statementinterface.h index 327c4096d..8d092351f 100644 --- a/kmymoney/plugins/statementinterface.h +++ b/kmymoney/plugins/statementinterface.h @@ -1,72 +1,75 @@ -/*************************************************************************** - statementinterface.h - ------------------- - begin : Wed Jan 5 2005 - copyright : (C) 2005 Thomas Baumgart - email : ipwizard@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. * - * * - ***************************************************************************/ +/* + * Copyright 2005-2018 Thomas Baumgart + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #ifndef STATEMENTINTERFACE_H #define STATEMENTINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include class MyMoneyAccount; class MyMoneyStatement; class MyMoneyKeyValueContainer; namespace KMyMoneyPlugin { /** * This abstract class represents the interface to import statements * into the KMyMoney application */ class KMM_PLUGIN_EXPORT StatementInterface : public QObject { Q_OBJECT public: explicit StatementInterface(QObject* parent, const char* name = 0); virtual ~StatementInterface(); + virtual void resetMessages() const = 0; + virtual void showMessages(int statementCount) const = 0; + /** * This method imports a MyMoneyStatement into the engine */ virtual QStringList import(const MyMoneyStatement& s, bool silent = false) = 0; /** * This method returns the account for a given @a key - @a value pair. * If the account is not found in the list of accounts, MyMoneyAccount() * is returned. */ virtual MyMoneyAccount account(const QString& key, const QString& value) const = 0; /** */ virtual void setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const = 0; }; } // namespace #endif