/*************************************************************************** mymoneystatementreader.cpp ------------------- begin : Mon Aug 30 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifdef HAVE_CONFIG_H #include #endif #include // ---------------------------------------------------------------------------- // TQt Headers #include #include #include #include // ---------------------------------------------------------------------------- // TDE Headers #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneystatementreader.h" #include #include #include #include #include #include "../dialogs/kaccountselectdlg.h" #include "../dialogs/transactionmatcher.h" #include "../dialogs/kenterscheduledlg.h" #include "../kmymoney2.h" #include class MyMoneyStatementReader::Private { public: Private() : transactionsCount(0), transactionsAdded(0), transactionsMatched(0), transactionsDuplicate(0), scannedCategories(false) {} const TQString& feeId(const MyMoneyAccount& invAcc); const TQString& interestId(const MyMoneyAccount& invAcc); TQString interestId(const TQString& name); TQString feeId(const TQString& name); void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in); MyMoneyAccount lastAccount; TQValueList transactions; TQValueList payees; int transactionsCount; int transactionsAdded; int transactionsMatched; int transactionsDuplicate; TQMap uniqIds; TQMap securitiesBySymbol; TQMap securitiesByName; bool m_skipCategoryMatching; private: void scanCategories(TQString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const TQString& defaultName); TQString nameToId(const TQString&name, MyMoneyAccount& parent); private: TQString m_feeId; TQString m_interestId; bool scannedCategories; }; const TQString& MyMoneyStatementReader::Private::feeId(const MyMoneyAccount& invAcc) { scanCategories(m_feeId, invAcc, MyMoneyFile::instance()->expense(), i18n("_Fees")); return m_feeId; } const TQString& MyMoneyStatementReader::Private::interestId(const MyMoneyAccount& invAcc) { scanCategories(m_interestId, invAcc, MyMoneyFile::instance()->income(), i18n("_Dividend")); return m_interestId; } TQString MyMoneyStatementReader::Private::nameToId(const TQString&name, MyMoneyAccount& parent) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount acc = file->accountByName(name); // if it does not exist, we have to create it if(acc.id().isEmpty()) { acc.setName( name ); acc.setAccountType( parent.accountType() ); acc.setCurrencyId(parent.currencyId()); file->addAccount(acc, parent); } return acc.id(); } TQString MyMoneyStatementReader::Private::interestId(const TQString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->income(); return nameToId(name, parent); } TQString MyMoneyStatementReader::Private::feeId(const TQString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->expense(); return nameToId(name, parent); } void MyMoneyStatementReader::Private::scanCategories(TQString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const TQString& defaultName) { if(!scannedCategories) { KMyMoneyUtils::previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId); scannedCategories = true; } if(id.isEmpty()) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount acc = file->accountByName(defaultName); // if it does not exist, we have to create it if(acc.id().isEmpty()) { MyMoneyAccount parent = parentAccount; acc.setName( defaultName ); acc.setAccountType( parent.accountType() ); acc.setCurrencyId(parent.currencyId()); file->addAccount(acc, parent); } id = acc.id(); } } void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in) { if( ! t_in.m_strBankID.isEmpty() ) { // make sure that id's are unique from this point on by appending a -# // postfix if needed TQString base(t_in.m_strBankID); TQString hash(base); int idx = 1; for(;;) { TQMap::const_iterator it; it = uniqIds.find(hash); if(it == uniqIds.end()) { uniqIds[hash] = true; break; } hash = TQString("%1-%2").arg(base).arg(idx); ++idx; } s.setBankID(hash); } } MyMoneyStatementReader::MyMoneyStatementReader() : d(new Private), m_userAbort(false), m_autoCreatePayee(false), m_ft(0), m_progressCallback(0) { m_askPayeeCategory = KMyMoneyGlobalSettings::askForPayeeCategory(); } MyMoneyStatementReader::~MyMoneyStatementReader() { delete d; } bool MyMoneyStatementReader::anyTransactionAdded(void) const { return (d->transactionsAdded != 0) ? true : false; } void MyMoneyStatementReader::setAutoCreatePayee(bool create) { m_autoCreatePayee = create; } void MyMoneyStatementReader::setAskPayeeCategory(bool ask) { m_askPayeeCategory = ask; } bool MyMoneyStatementReader::import(const MyMoneyStatement& s, TQStringList& messages) { // // For testing, save the statement to an XML file // (uncomment this line) // //MyMoneyStatement::writeXMLFile(s,"Imported.Xml"); // // Select the account // m_account = MyMoneyAccount(); m_ft = new MyMoneyFileTransaction(); d->m_skipCategoryMatching = s.m_skipCategoryMatching; // if the statement source left some information about // the account, we use it to get the current data of it if(!s.m_accountId.isEmpty()) { try { m_account = MyMoneyFile::instance()->account(s.m_accountId); } catch(MyMoneyException* e) { tqDebug(TQString("Received reference '%1' to unknown account in statement").arg(s.m_accountId)); delete e; } } if(m_account.id().isEmpty()) { m_account.setName(s.m_strAccountName); m_account.setNumber(s.m_strAccountNumber); switch ( s.m_eType ) { case MyMoneyStatement::etCheckings: m_account.setAccountType(MyMoneyAccount::Checkings); break; case MyMoneyStatement::etSavings: m_account.setAccountType(MyMoneyAccount::Savings); break; case MyMoneyStatement::etInvestment: //testing support for investment statements! //m_userAbort = true; //KMessageBox::error(kmymoney2, i18n("This is an investment statement. These are not supported currently."), i18n("Critical Error")); m_account.setAccountType(MyMoneyAccount::Investment); break; case MyMoneyStatement::etCreditCard: m_account.setAccountType(MyMoneyAccount::CreditCard); break; default: m_account.setAccountType(MyMoneyAccount::Checkings); break; } // we ask the user only if we have some transactions to process if ( !m_userAbort && s.m_listTransactions.count() > 0) m_userAbort = ! selectOrCreateAccount(Select, m_account); } // see if we need to update some values stored with the account if(m_account.value("lastStatementBalance") != s.m_closingBalance.toString() || m_account.value("lastImportedTransactionDate") != s.m_dateEnd.toString(TQt::ISODate)) { if(s.m_closingBalance != MyMoneyMoney::autoCalc) { m_account.setValue("lastStatementBalance", s.m_closingBalance.toString()); if ( s.m_dateEnd.isValid() ) { m_account.setValue("lastImportedTransactionDate", s.m_dateEnd.toString(TQt::ISODate)); } } try { MyMoneyFile::instance()->modifyAccount(m_account); } catch(MyMoneyException* e) { tqDebug("Updating account in MyMoneyStatementReader::startImport failed"); delete e; } } if(!m_account.name().isEmpty()) messages += i18n("Importing statement for account %1").arg(m_account.name()); else if(s.m_listTransactions.count() == 0) messages += i18n("Importing statement without transactions"); tqDebug(TQString("Importing statement for '%1'").arg(m_account.name())); // // Process the securities // signalProgress(0, s.m_listSecurities.count(), "Importing Statement ..."); int progress = 0; TQValueList::const_iterator it_s = s.m_listSecurities.begin(); while ( it_s != s.m_listSecurities.end() ) { processSecurityEntry(*it_s); signalProgress(++progress, 0); ++it_s; } signalProgress(-1, -1); // // Process the transactions // if ( !m_userAbort ) { try { tqDebug(TQString("Processing transactions (%1)").arg(m_account.name())); signalProgress(0, s.m_listTransactions.count(), "Importing Statement ..."); int progress = 0; TQValueList::const_iterator it_t = s.m_listTransactions.begin(); while ( it_t != s.m_listTransactions.end() ) { processTransactionEntry(*it_t); signalProgress(++progress, 0); ++it_t; } tqDebug(TQString("Processing transactions done (%1)").arg(m_account.name())); } catch(MyMoneyException* e) { if(e->what() == "USERABORT") m_userAbort = true; else tqDebug(TQString("Caught exception from processTransactionEntry() not caused by USERABORT: %1").arg(e->what())); delete e; } signalProgress(-1, -1); } // // process price entries // if ( !m_userAbort ) { try { signalProgress(0, s.m_listPrices.count(), "Importing Statement ..."); TQValueList slist = MyMoneyFile::instance()->securityList(); TQValueList::const_iterator it_s; for(it_s = slist.begin(); it_s != slist.end(); ++it_s) { d->securitiesBySymbol[(*it_s).tradingSymbol()] = *it_s; d->securitiesByName[(*it_s).name()] = *it_s; } int progress = 0; TQValueList::const_iterator it_p = s.m_listPrices.begin(); while(it_p != s.m_listPrices.end()) { processPriceEntry(*it_p); signalProgress(++progress, 0); ++it_p; } } catch(MyMoneyException* e) { if(e->what() == "USERABORT") m_userAbort = true; else tqDebug(TQString("Caught exception from processPriceEntry() not caused by USERABORT: %1").arg(e->what())); delete e; } signalProgress(-1, -1); } bool rc = false; // delete all payees created in vain int payeeCount = d->payees.count(); TQValueList::const_iterator it_p; for(it_p = d->payees.begin(); it_p != d->payees.end(); ++it_p) { try { MyMoneyFile::instance()->removePayee(*it_p); --payeeCount; } catch(MyMoneyException* e) { // if we can't delete it, it must be in use which is ok for us delete e; } } if(s.m_closingBalance.isAutoCalc()) { messages += i18n(" Statement balance is not contained in statement."); } else { messages += i18n(" Statement balance on %1 is reported to be %2").arg(s.m_dateEnd.toString(TQt::ISODate)).arg(s.m_closingBalance.formatMoney("",2)); } messages += i18n(" Transactions"); messages += i18n(" %1 processed").arg(d->transactionsCount); messages += i18n(" %1 added").arg(d->transactionsAdded); messages += i18n(" %1 matched").arg(d->transactionsMatched); messages += i18n(" %1 duplicates").arg(d->transactionsDuplicate); messages += i18n(" Payees"); messages += i18n(" %1 created").arg(payeeCount); messages += TQString(); // remove the Don't ask again entries TDEConfig* config = TDEGlobal::config(); config->setGroup(TQString::fromLatin1("Notification Messages")); TQStringList::ConstIterator it; for(it = m_dontAskAgain.begin(); it != m_dontAskAgain.end(); ++it) { config->deleteEntry(*it); } config->sync(); m_dontAskAgain.clear(); rc = !m_userAbort; // finish the transaction if(rc) m_ft->commit(); delete m_ft; m_ft = 0; tqDebug(TQString("Importing statement for '%1' done").arg(m_account.name())); return rc; } void MyMoneyStatementReader::processPriceEntry(const MyMoneyStatement::Price& p_in) { if(d->securitiesBySymbol.contains(p_in.m_strSecurity)) { MyMoneyPrice price(d->securitiesBySymbol[p_in.m_strSecurity].id(), MyMoneyFile::instance()->baseCurrency().id(), p_in.m_date, p_in.m_amount, "QIF"); MyMoneyFile::instance()->addPrice(price); } else if(d->securitiesByName.contains(p_in.m_strSecurity)) { MyMoneyPrice price(d->securitiesByName[p_in.m_strSecurity].id(), MyMoneyFile::instance()->baseCurrency().id(), p_in.m_date, p_in.m_amount, "QIF"); MyMoneyFile::instance()->addPrice(price); } } void MyMoneyStatementReader::processSecurityEntry(const MyMoneyStatement::Security& sec_in) { // For a security entry, we will just make sure the security exists in the // file. It will not get added to the investment account until it's called // for in a transaction. MyMoneyFile* file = MyMoneyFile::instance(); // check if we already have the security // In a statement, we do not know what type of security this is, so we will // not use type as a matching factor. MyMoneySecurity security; TQValueList list = file->securityList(); TQValueList::ConstIterator it = list.begin(); while ( it != list.end() && security.id().isEmpty() ) { if(sec_in.m_strSymbol.isEmpty()) { if((*it).name() == sec_in.m_strName) security = *it; } else if((*it).tradingSymbol() == sec_in.m_strSymbol) security = *it; ++it; } // if the security was not found, we have to create it while not forgetting // to setup the type if(security.id().isEmpty()) { security.setName(sec_in.m_strName); security.setTradingSymbol(sec_in.m_strSymbol); security.setSmallestAccountFraction(1000); security.setTradingCurrency(file->baseCurrency().id()); security.setValue("kmm-security-id", sec_in.m_strId); security.setValue("kmm-online-source", "Yahoo"); security.setSecurityType(MyMoneySecurity::SECURITY_STOCK); MyMoneyFileTransaction ft; try { file->addSecurity(security); ft.commit(); kdDebug(0) << "Created " << security.name() << " with id " << security.id() << endl; } catch(MyMoneyException *e) { KMessageBox::error(0, i18n("Error creating security record: %1").arg(e->what()), i18n("Error")); } } else { kdDebug(0) << "Found " << security.name() << " with id " << security.id() << endl; } } void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& t_in) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyTransaction t; #if 0 TQString dbgMsg; dbgMsg = TQString("Process %1, '%3', %2").arg(t_in.m_datePosted.toString(TQt::ISODate)).arg(t_in.m_amount.formatMoney("", 2)).arg(t_in.m_strBankID); tqDebug(dbgMsg); #endif // mark it imported for the view t.setImported(); // TODO (Ace) We can get the commodity from the statement!! // Although then we would need UI to verify t.setCommodity(m_account.currencyId()); t.setPostDate(t_in.m_datePosted); t.setMemo(t_in.m_strMemo); #if 0 // (acejones) removing this code. keeping it around for reference. // // this is the OLD way of handling bank ID's, which unfortunately was wrong. // bank ID's actually need to go on the split which corresponds with the // account we're importing into. // // thus anywhere "this account" is put into a split is also where we need // to put the bank ID in. // if ( ! t_in.m_strBankID.isEmpty() ) t.setBankID(t_in.m_strBankID); #endif MyMoneySplit s1; s1.setMemo(t_in.m_strMemo); s1.setValue(t_in.m_amount - t_in.m_fees); s1.setShares(s1.value()); s1.setNumber(t_in.m_strNumber); // set these values if a transfer split is needed at the very end. MyMoneyMoney transfervalue; // If the user has chosen to import into an investment account, determine the correct account to use MyMoneyAccount thisaccount = m_account; TQString brokerageactid; if ( thisaccount.accountType() == MyMoneyAccount::Investment ) { // determine the brokerage account brokerageactid = m_account.value("kmm-brokerage-account").utf8(); if (brokerageactid.isEmpty() ) { brokerageactid = file->accountByName(m_account.brokerageName()).id(); } // find the security transacted, UNLESS this transaction didn't // involve any security. if ( (t_in.m_eAction != MyMoneyStatement::Transaction::eaNone) && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest) && (t_in.m_eAction != MyMoneyStatement::Transaction::eaFees)) { // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and // (2a) the symbol of the underlying security matches the security of the // transaction, or // (2b) the name of the security matches the name of the security of the transaction. // search through each subordinate account bool found = false; TQStringList accounts = thisaccount.accountList(); TQStringList::const_iterator it_account = accounts.begin(); while( !found && it_account != accounts.end() ) { TQString currencyid = file->account(*it_account).currencyId(); MyMoneySecurity security = file->security( currencyid ); if((t_in.m_strSymbol.lower() == security.tradingSymbol().lower()) || (t_in.m_strSecurity.lower() == security.name().lower())) { thisaccount = file->account(*it_account); found = true; // Don't update price if there is no price information contained in the transaction if(t_in.m_eAction != MyMoneyStatement::Transaction::eaCashDividend && t_in.m_eAction != MyMoneyStatement::Transaction::eaShrsin && t_in.m_eAction != MyMoneyStatement::Transaction::eaShrsout) { // update the price, while we're here. in the future, this should be // an option TQString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price( currencyid, basecurrencyid, t_in.m_datePosted, true ); if ( !price.isValid() && ((!t_in.m_amount.isZero() && !t_in.m_shares.isZero()) || !t_in.m_price.isZero())) { MyMoneyPrice newprice; if(!t_in.m_price.isZero()) { newprice = MyMoneyPrice( currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_price.abs(), i18n("Statement Importer") ); } else { newprice = MyMoneyPrice( currencyid, basecurrencyid, t_in.m_datePosted, (t_in.m_amount / t_in.m_shares).abs(), i18n("Statement Importer") ); } file->addPrice(newprice); } } } ++it_account; } // If there was no stock account under the m_acccount investment account, // add one using the security. if (!found) { // The security should always be available, because the statement file // should separately list all the securities referred to in the file, // and when we found a security, we added it to the file. if ( t_in.m_strSecurity.isEmpty() ) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored.").arg(t_in.m_strSecurity),i18n("Security not found"),TQString("BlankSecurity")); return; } else { MyMoneySecurity security; TQValueList list = MyMoneyFile::instance()->securityList(); TQValueList::ConstIterator it = list.begin(); while ( it != list.end() && security.id().isEmpty() ) { if(t_in.m_strSecurity.lower() == (*it).tradingSymbol().lower() || t_in.m_strSecurity.lower() == (*it).name().lower()) { security = *it; } ++it; } if(!security.id().isEmpty()) { thisaccount = MyMoneyAccount(); thisaccount.setName(security.name()); thisaccount.setAccountType(MyMoneyAccount::Stock); thisaccount.setCurrencyId(security.id()); file->addAccount(thisaccount, m_account); kdDebug(0) << __func__ << ": created account " << thisaccount.id() << " for security " << t_in.m_strSecurity << " under account " << m_account.id() << endl; } // this security does not exist in the file. else { // This should be rare. A statement should have a security entry for any // of the securities referred to in the transactions. The only way to get // here is if that's NOT the case. KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. Transactions involving this security will be ignored.").arg(t_in.m_strSecurity),i18n("Security not found"),TQString("MissingSecurity%1").arg(t_in.m_strSecurity.stripWhiteSpace())); return; } } } } s1.setAccountId(thisaccount.id()); d->assignUniqueBankID(s1, t_in); if (t_in.m_eAction==MyMoneyStatement::Transaction::eaReinvestDividend) { s1.setAction(MyMoneySplit::ActionReinvestDividend); s1.setShares(t_in.m_shares); if(!t_in.m_price.isZero()) { s1.setPrice(t_in.m_price); } else { if(t_in.m_shares.isZero()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no share amount. These transactions will be ignored."), i18n("No share amount provided"), TQString("BlankAmount")); return; } s1.setPrice(((t_in.m_amount - t_in.m_fees) / t_in.m_shares).convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()))); } MyMoneySplit s2; s2.setMemo(t_in.m_strMemo); if(t_in.m_strInterestCategory.isEmpty()) s2.setAccountId(d->interestId(thisaccount)); else s2.setAccountId(d->interestId(t_in.m_strInterestCategory)); s2.setShares(-t_in.m_amount - t_in.m_fees); s2.setValue(s2.shares()); t.addSplit(s2); } else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaCashDividend) { // Cash dividends require setting 2 splits to get all of the information // in. Split #1 will be the income split, and we'll set it to the first // income account. This is a hack, but it's needed in order to get the // amount into the transaction. // There are some sign issues. The OFX plugin universally reverses the sign // for investment transactions. // // The way we interpret the sign on 'amount' is the s1 split, which is always // the thing that's NOT the cash account. For dividends, it's the income // category, for buy/sell it's the stock account. // // For cash account transactions, the s1 split IS the cash account split, // which explains why they have to be reversed for investment transactions // // Ergo, the 'amount' is negative at this point and needs to stay negative. // The 'fees' is positive. // // This should probably change. It would be more consistent to ALWAYS // interpret the 'amount' as the cash account part. if(t_in.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else s1.setAccountId(d->interestId(t_in.m_strInterestCategory)); s1.setShares(t_in.m_amount); s1.setValue(t_in.m_amount); // Split 2 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. MyMoneySplit s2; s2.setMemo(t_in.m_strMemo); s2.setAction(MyMoneySplit::ActionDividend); s2.setAccountId(thisaccount.id()); t.addSplit(s2); transfervalue = -t_in.m_amount - t_in.m_fees; } else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaInterest) { if(t_in.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else s1.setAccountId(d->interestId(t_in.m_strInterestCategory)); s1.setShares(t_in.m_amount); s1.setValue(t_in.m_amount); transfervalue = -t_in.m_amount; } else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaFees) { if(t_in.m_strInterestCategory.isEmpty()) s1.setAccountId(d->feeId(thisaccount)); else s1.setAccountId(d->feeId(t_in.m_strInterestCategory)); s1.setShares(t_in.m_amount); s1.setValue(t_in.m_amount); transfervalue = -t_in.m_amount; } else if ((t_in.m_eAction==MyMoneyStatement::Transaction::eaBuy ) || (t_in.m_eAction==MyMoneyStatement::Transaction::eaSell)) { if(!t_in.m_price.isZero()) { s1.setPrice(t_in.m_price.abs()); } else { MyMoneyMoney total; total = t_in.m_amount - t_in.m_fees; if(!t_in.m_shares.isZero()) s1.setPrice((total / t_in.m_shares).abs().convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()))); } s1.setAction(MyMoneySplit::ActionBuyShares); // Make sure to setup the sign correctly if(t_in.m_eAction==MyMoneyStatement::Transaction::eaBuy ) { s1.setShares(t_in.m_shares.abs()); s1.setValue(s1.value().abs()); transfervalue = -(t_in.m_amount.abs()); } else { s1.setShares(-(t_in.m_shares.abs())); s1.setValue(-(s1.value().abs())); transfervalue = t_in.m_amount.abs(); } } else if ((t_in.m_eAction==MyMoneyStatement::Transaction::eaShrsin) || (t_in.m_eAction==MyMoneyStatement::Transaction::eaShrsout)) { s1.setValue(MyMoneyMoney()); s1.setShares(t_in.m_shares); s1.setAction(MyMoneySplit::ActionAddShares); } else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaNone) { // User is attempting to import a non-investment transaction into this // investment account. This is not supportable the way KMyMoney is // written. However, if a user has an associated brokerage account, // we can stuff the transaction there. TQString brokerageactid = m_account.value("kmm-brokerage-account").utf8(); if (brokerageactid.isEmpty() ) { brokerageactid = file->accountByName(m_account.brokerageName()).id(); } if ( ! brokerageactid.isEmpty() ) { s1.setAccountId(brokerageactid); d->assignUniqueBankID(s1, t_in); // Needed to satisfy the bankid check below. thisaccount = file->account(brokerageactid); } else { // Warning!! Your transaction is being thrown away. } } if ( !t_in.m_fees.isZero() ) { MyMoneySplit s; s.setMemo(i18n("(Fees) ") + t_in.m_strMemo); s.setValue(t_in.m_fees); s.setShares(t_in.m_fees); s.setAccountId(d->feeId(thisaccount)); t.addSplit(s); } } else { // For non-investment accounts, just use the selected account // Note that it is perfectly reasonable to import an investment statement into a non-investment account // if you really want. The investment-specific information, such as number of shares and action will // be discarded in that case. s1.setAccountId(m_account.id()); d->assignUniqueBankID(s1, t_in); } TQString payeename = t_in.m_strPayee; if(!payeename.isEmpty()) { TQString payeeid; try { TQValueList pList = file->payeeList(); TQValueList::const_iterator it_p; TQMap matchMap; for(it_p = pList.begin(); it_p != pList.end(); ++it_p) { bool ignoreCase; TQStringList keys; TQStringList::const_iterator it_s; switch((*it_p).matchData(ignoreCase, keys)) { case MyMoneyPayee::matchDisabled: break; case MyMoneyPayee::matchName: keys << TQString("%1").arg(TQRegExp::escape((*it_p).name())); // tricky fall through here case MyMoneyPayee::matchKey: for(it_s = keys.begin(); it_s != keys.end(); ++it_s) { TQRegExp exp(*it_s, !ignoreCase); if(exp.search(payeename) != -1) { matchMap[exp.matchedLength()] = (*it_p).id(); } } break; } } // at this point we can have several scenarios: // a) multiple matches // b) a single match // c) no match at all // // for c) we just do nothing, for b) we take the one we found // in case of a) we take the one with the largest matchedLength() // which happens to be the last one in the map if(matchMap.count() > 1) { TQMap::const_iterator it_m = matchMap.end(); --it_m; payeeid = *it_m; } else if(matchMap.count() == 1) payeeid = *(matchMap.begin()); // if we did not find a matching payee, we throw an exception and try to create it if(payeeid.isEmpty()) throw new MYMONEYEXCEPTION("payee not matched"); s1.setPayeeId(payeeid); } catch (MyMoneyException *e) { MyMoneyPayee payee; int rc = KMessageBox::Yes; if(m_autoCreatePayee == false) { // Ask the user if that is what he intended to do? TQString msg = i18n("Do you want to add \"%1\" as payee/receiver?\n\n").arg(payeename); msg += i18n("Selecting \"Yes\" will create the payee, \"No\" will skip " "creation of a payee record and remove the payee information " "from this transaction. Selecting \"Cancel\" aborts the import " "operation.\n\nIf you select \"No\" here and mark the \"Don't ask " "again\" checkbox, the payee information for all following transactions " "referencing \"%1\" will be removed.").arg(payeename); TQString askKey = TQString("Statement-Import-Payee-")+payeename; if(!m_dontAskAgain.contains(askKey)) { m_dontAskAgain += askKey; } rc = KMessageBox::questionYesNoCancel(0, msg, i18n("New payee/receiver"), KStdGuiItem::yes(), KStdGuiItem::no(), askKey); } delete e; if(rc == KMessageBox::Yes) { // for now, we just add the payee to the pool and turn // on simple name matching, so that future transactions // with the same name don't get here again. // // In the future, we could open a dialog and ask for // all the other attributes of the payee, but since this // is called in the context of an automatic procedure it // might distract the user. payee.setName(payeename); payee.setMatchData(MyMoneyPayee::matchName, true, TQStringList()); if (m_askPayeeCategory) { // We use a TQGuardedPtr because the dialog may get deleted // during exec() if the parent of the dialog gets deleted. // In that case the guarded ptr will reset to 0. TQGuardedPtr dialog = new KDialogBase( "Default Category for Payee", KDialogBase::Yes | KDialogBase::No | KDialogBase::Cancel, KDialogBase::Yes, KDialogBase::Cancel, 0, "questionYesNoCancel", true, true, KGuiItem(i18n("Save Category")), KGuiItem(i18n("No Category")), KGuiItem(i18n("Abort"))); TQVBox *topcontents = new TQVBox (dialog); topcontents->setSpacing(KDialog::spacingHint()*2); topcontents->setMargin(KDialog::marginHint()); //add in caption? and account combo here TQLabel *label1 = new TQLabel( topcontents); label1->setText(i18n("Please select a default category for payee '%1':").arg(payee.name())); TQGuardedPtr accountCombo = new KMyMoneyAccountCombo(topcontents); dialog->setMainWidget(topcontents); int result = dialog->exec(); TQString accountId; if (accountCombo && !accountCombo->selectedAccounts().isEmpty()) { accountId = accountCombo->selectedAccounts().front(); } if (dialog) { delete dialog; } //if they hit yes instead of no, then grab setting of account combo if (result == KDialogBase::Yes) { payee.setDefaultAccountId(accountId); } else if (result != KDialogBase::No) { //add cancel button? and throw exception like below throw new MYMONEYEXCEPTION("USERABORT"); } } try { file->addPayee(payee); tqDebug(TQString("Payee '%1' created").arg(payee.name())); d->payees << payee; payeeid = payee.id(); s1.setPayeeId(payeeid); } catch(MyMoneyException *e) { KMessageBox::detailedSorry(0, i18n("Unable to add payee/receiver"), (e->what() + " " + i18n("thrown in") + " " + e->file()+ ":%1").arg(e->line())); delete e; } } else if(rc == KMessageBox::No) { s1.setPayeeId(TQString()); } else { throw new MYMONEYEXCEPTION("USERABORT"); } } if(thisaccount.accountType() != MyMoneyAccount::Stock ) { // // Fill in other side of the transaction (category/etc) based on payee // // Note, this logic is lifted from KLedgerView::slotPayeeChanged(), // however this case is more complicated, because we have an amount and // a memo. We just don't have the other side of the transaction. // // We'll search for the most recent transaction in this account with // this payee. If this reference transaction is a simple 2-split // transaction, it's simple. If it's a complex split, and the amounts // are different, we have a problem. Somehow we have to balance the // transaction. For now, we'll leave it unbalanced, and let the user // handle it. // const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeid); if (t_in.m_listSplits.isEmpty() && payeeObj.defaultAccountEnabled()) { MyMoneySplit s; s.setReconcileFlag(MyMoneySplit::Cleared); s.clearId(); s.setBankID(TQString()); s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setAccountId(payeeObj.defaultAccountId()); t.addSplit(s); } else if (t_in.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) { MyMoneyTransactionFilter filter(thisaccount.id()); filter.addPayee(payeeid); TQValueList list = file->transactionList(filter); if(!list.empty()) { // Default to using the most recent transaction as the reference MyMoneyTransaction t_old = list.last(); // if there is more than one matching transaction, try to be a little // smart about which one we take. for now, we'll see if there's one // with the same VALUE as our imported transaction, and if so take that one. if ( list.count() > 1 ) { TQValueList::ConstIterator it_trans = list.fromLast(); while ( it_trans != list.end() ) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if ( s.value() == s1.value() ) { t_old = *it_trans; break; } --it_trans; } } TQValueList::ConstIterator it_split; for(it_split = t_old.splits().begin(); it_split != t_old.splits().end(); ++it_split) { // We don't need the split that covers this account, // we just need the other ones. if ( (*it_split).accountId() != thisaccount.id() ) { MyMoneySplit s(*it_split); s.setReconcileFlag(MyMoneySplit::NotReconciled); s.clearId(); s.setBankID(TQString()); if ( t_old.splits().count() == 2 ) { s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setMemo(s1.memo()); } t.addSplit(s); } } } } } } s1.setReconcileFlag(t_in.m_reconcile); t.addSplit(s1); // Add the 'account' split if it's needed if ( ! transfervalue.isZero() ) { // in case the transaction has a reference to the brokerage account, we use it if(!t_in.m_strBrokerageAccount.isEmpty()) { brokerageactid = file->accountByName(t_in.m_strBrokerageAccount).id(); } if ( !brokerageactid.isEmpty() ) { // FIXME This may not deal with foreign currencies properly MyMoneySplit s; s.setMemo(t_in.m_strMemo); s.setValue(transfervalue); s.setShares(transfervalue); s.setAccountId(brokerageactid); s.setReconcileFlag(t_in.m_reconcile); t.addSplit(s); } } if ((t_in.m_eAction != MyMoneyStatement::Transaction::eaReinvestDividend) && (t_in.m_eAction!=MyMoneyStatement::Transaction::eaCashDividend) ) { //****************************************** // process splits //****************************************** TQValueList::const_iterator it_s; for(it_s = t_in.m_listSplits.begin(); it_s != t_in.m_listSplits.end(); ++it_s) { MyMoneySplit s2; s2.setAccountId((*it_s).m_accountId); MyMoneyAccount acc = file->account(s2.accountId()); if(acc.isAssetLiability()) { s2.setPayeeId(s1.payeeId()); } s2.setMemo((*it_s).m_strMemo); s2.setShares((*it_s).m_amount); s2.setValue((*it_s).m_amount); s2.setReconcileFlag((*it_s).m_reconcile); t.addSplit(s2); } #if 0 TQString accountId; int count; int cnt = 0; count = t_in.m_listSplits.count(); for(cnt = 0; cnt < count; ++cnt ) { MyMoneySplit s2 = s1; s2.setMemo(t_in.m_listSplits[cnt].m_strMemo); s2.clearId(); s2.setValue(t_in.m_listSplits[cnt].m_amount); s2.setShares(t_in.m_listSplits[cnt].m_amount); s2.setAccountId(TQString(t_in.m_listSplits[cnt].m_accountId)); #if 0 accountId = file->nameToAccount(t_in.m_listSplits[cnt].m_strCategoryName); if (accountId.isEmpty()) accountId = checkCategory(t_in.m_listSplits[cnt].m_strCategoryName, t_in.m_listSplits[0].m_amount, t_in.m_listSplits[cnt].m_amount); s2.setAccountId(accountId); #endif t.addSplit(s2); } #endif } // Add the transaction try { // check for matches already stored in the engine MyMoneySplit matchedSplit; TransactionMatcher::autoMatchResultE result; TransactionMatcher matcher(thisaccount); matcher.setMatchWindow(KMyMoneyGlobalSettings::matchInterval()); const MyMoneyObject *o = matcher.findMatch(t, s1, matchedSplit, result); d->transactionsCount++; // if we did not already find this one, we need to process it if(result != TransactionMatcher::matchedDuplicate) { d->transactionsAdded++; file->addTransaction(t); if(o) { if(typeid(*o) == typeid(MyMoneyTransaction)) { // it matched a simple transaction. that's the easy case MyMoneyTransaction tm(*(dynamic_cast(o))); switch(result) { case TransactionMatcher::notMatched: case TransactionMatcher::matchedDuplicate: // no need to do anything here break; case TransactionMatcher::matched: case TransactionMatcher::matchedExact: tqDebug(TQString("Detected as match to transaction '%1'").arg(tm.id())); matcher.match(tm, matchedSplit, t, s1, true); d->transactionsMatched++; break; } } else if(typeid(*o) == typeid(MyMoneySchedule)) { // a match has been found in a pending schedule. We'll ask the user if she wants // to enter the schedule and match it agains the new transaction. Otherwise, we // just leave the transaction as imported. MyMoneySchedule schedule(*(dynamic_cast(o))); if(KMessageBox::questionYesNo(0, TQString("%1").arg(i18n("KMyMoney has found a scheduled transaction named %1 which matches an imported transaction. Do you want KMyMoney to enter this schedule now so that the transaction can be matched? ").arg(schedule.name())), i18n("Schedule found")) == KMessageBox::Yes) { KEnterScheduleDlg dlg(0, schedule); TransactionEditor* editor = dlg.startEdit(); if(editor) { MyMoneyTransaction torig; // in case the amounts of the scheduled transaction and the // imported transaction differ, we need to update the amount // using the transaction editor. if(matchedSplit.shares() != s1.shares() && !schedule.isFixed()) { // for now this only works with regular transactions and not // for investment transactions. As of this, we don't have // scheduled investment transactions anyway. StdTransactionEditor* se = dynamic_cast(editor); if(se) { // the following call will update the amount field in the // editor and also adjust a possible VAT assignment. Make // sure to use only the absolute value of the amount, because // the editor keeps the sign in a different position (deposit, // withdrawal tab) kMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount")); if(amount) { amount->setValue(s1.shares().abs()); se->slotUpdateAmount(s1.shares().abs().toString()); // we also need to update the matchedSplit variable to // have the modified share/value. matchedSplit.setShares(s1.shares()); matchedSplit.setValue(s1.value()); } } } editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits()[0], true); TQString newId; if(editor->enterTransactions(newId, false, true)) { if(!newId.isEmpty()) { torig = MyMoneyFile::instance()->transaction(newId); schedule.setLastPayment(torig.postDate()); } schedule.setNextDueDate(schedule.nextPayment(schedule.nextDueDate())); MyMoneyFile::instance()->modifySchedule(schedule); } // now match the two transactions matcher.match(torig, matchedSplit, t, s1); d->transactionsMatched++; } delete editor; } } } } else { d->transactionsDuplicate++; tqDebug("Detected as duplicate"); } delete o; } catch (MyMoneyException *e) { TQString message(i18n("Problem adding or matching imported transaction with id '%1': %2").arg(t_in.m_strBankID).arg(e->what())); tqDebug(message); delete e; int result = KMessageBox::warningContinueCancel(0, message); if ( result == KMessageBox::Cancel ) throw new MYMONEYEXCEPTION("USERABORT"); } } bool MyMoneyStatementReader::selectOrCreateAccount(const SelectCreateMode /*mode*/, MyMoneyAccount& account) { bool result = false; MyMoneyFile* file = MyMoneyFile::instance(); TQString accountId; // Try to find an existing account in the engine which matches this one. // There are two ways to be a "matching account". The account number can // match the statement account OR the "StatementKey" property can match. // Either way, we'll update the "StatementKey" property for next time. TQString accountNumber = account.number(); if ( ! accountNumber.isEmpty() ) { // Get a list of all accounts TQValueList accounts; file->accountList(accounts); // Iterate through them TQValueList::const_iterator it_account = accounts.begin(); while ( it_account != accounts.end() ) { if ( ( (*it_account).value("StatementKey") == accountNumber ) || ( (*it_account).number() == accountNumber ) ) { MyMoneyAccount newAccount((*it_account).id(), account); account = newAccount; accountId = (*it_account).id(); break; } ++it_account; } } TQString msg = i18n("You have downloaded a statement for the following account:

"); msg += i18n(" - Account Name: %1").arg(account.name()) + "
"; msg += i18n(" - Account Type: %1").arg(KMyMoneyUtils::accountTypeToString(account.accountType())) + "
"; msg += i18n(" - Account Number: %1").arg(account.number()) + "
"; msg += "
"; TQString header; if(!account.name().isEmpty()) { if(!accountId.isEmpty()) msg += i18n("Do you want to import transactions to this account?"); else msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " "create a new account by pressing the Create button " "or select another one manually from the selection box below."); } else { msg += i18n("No account information has been found in the selected statement file. " "Please select an account using the selection box in the dialog or " "create a new account by pressing the Create button."); } KMyMoneyUtils::categoryTypeE type = static_cast(KMyMoneyUtils::asset|KMyMoneyUtils::liability); KAccountSelectDlg accountSelect(type, "StatementImport", kmymoney2); accountSelect.setHeader(i18n("Import transactions")); accountSelect.setDescription(msg); accountSelect.setAccount(account, accountId); accountSelect.setMode(false); accountSelect.showAbortButton(true); accountSelect.m_qifEntry->hide(); TQString accname; bool done = false; while ( !done ) { if ( accountSelect.exec() == TQDialog::Accepted && !accountSelect.selectedAccount().isEmpty() ) { result = true; done = true; accountId = accountSelect.selectedAccount(); account = file->account(accountId); if ( ! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber ) { account.setValue("StatementKey", accountNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(account); ft.commit(); accname = account.name(); } catch(MyMoneyException* e) { tqDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); delete e; } } } else { if(accountSelect.aborted()) //throw new MYMONEYEXCEPTION("USERABORT"); done = true; else KMessageBox::error(0, TQString("%1").arg(i18n("You must select an account, create a new one, or press the Abort button."))); } } return result; } void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const TQString&)) { m_progressCallback = callback; } void MyMoneyStatementReader::signalProgress(int current, int total, const TQString& msg) { if(m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } #include "mymoneystatementreader.moc"