You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kmymoney/kmymoney2/plugins/ofximport/ofximporterplugin.cpp

689 lines
20 KiB

/***************************************************************************
ofxiimporterplugin.cpp
-------------------
begin : Sat Jan 01 2005
copyright : (C) 2005 by Ace Jones
email : Ace Jones <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. *
* *
***************************************************************************/
// ----------------------------------------------------------------------------
// QT Includes
#include <tqfile.h>
#include <tqtextstream.h>
#include <tqradiobutton.h>
#include <tqspinbox.h>
#include <tqdatetimeedit.h>
// ----------------------------------------------------------------------------
// KDE Includes
#include <kgenericfactory.h>
#include <kdebug.h>
#include <tdefile.h>
#include <kurl.h>
#include <tdeaction.h>
#include <kmessagebox.h>
// ----------------------------------------------------------------------------
// Project Includes
#include "ofximporterplugin.h"
#include "konlinebankingstatus.h"
#include "konlinebankingsetupwizard.h"
#include "kofxdirectconnectdlg.h"
K_EXPORT_COMPONENT_FACTORY( kmm_ofximport,
KGenericFactory<OfxImporterPlugin>( "kmm_ofximport" ) )
OfxImporterPlugin::OfxImporterPlugin(TQObject *parent, const char *name, const TQStringList&) :
KMyMoneyPlugin::Plugin( parent, name ),
KMyMoneyPlugin::ImporterPlugin(),
m_valid( false )
{
setInstance(KGenericFactory<OfxImporterPlugin>::instance());
setXMLFile("kmm_ofximport.rc");
createActions();
}
OfxImporterPlugin::~OfxImporterPlugin()
{
}
void OfxImporterPlugin::createActions(void)
{
new TDEAction(i18n("OFX..."), "", 0, this, TQT_SLOT(slotImportFile()), actionCollection(), "file_import_ofx");
}
void OfxImporterPlugin::slotImportFile(void)
{
KURL url = importInterface()->selectFile(i18n("OFX import file selection"),
"",
"*.ofx *.qfx *.ofc|OFX files (*.ofx, *.qfx, *.ofc)\n*.*|All files (*.*)",
static_cast<KFile::Mode>(KFile::File | KFile::ExistingOnly));
if(url.isValid()) {
if ( isMyFormat(url.path()) ) {
slotImportFile(url.path());
} else {
KMessageBox::error( 0, i18n("Unable to import %1 using the OFX importer plugin. This file is not the correct format.").arg(url.prettyURL(0, KURL::StripFileProtocol)), i18n("Incorrect format"));
}
}
}
TQString OfxImporterPlugin::formatName(void) const
{
return "OFX";
}
TQString OfxImporterPlugin::formatFilenameFilter(void) const
{
return "*.ofx *.qfx *.ofc";
}
bool OfxImporterPlugin::isMyFormat( const TQString& filename ) const
{
// filename is considered an Ofx file if it contains
// the tag "<OFX>" or "<OFC>" in the first 20 lines
// which contain some data.
bool result = false;
TQFile f( filename );
if ( f.open( IO_ReadOnly ) )
{
TQTextStream ts( &f );
int lineCount = 20;
while ( !ts.atEnd() && !result && lineCount != 0)
{
// get a line of data and remove all unnecessary whitepace chars
TQString line = ts.readLine().simplifyWhiteSpace();
if ( line.contains("<OFX>",false)
|| line.contains("<OFC>",false) )
result = true;
// count only lines that contains some non white space chars
if(!line.isEmpty())
lineCount--;
}
f.close();
}
return result;
}
bool OfxImporterPlugin::import( const TQString& filename )
{
m_fatalerror = i18n("Unable to parse file");
m_valid = false;
m_errors.clear();
m_warnings.clear();
m_infos.clear();
m_statementlist.clear();
m_securitylist.clear();
TQCString filename_deep( filename.utf8() );
LibofxContextPtr ctx = libofx_get_new_context();
TQ_CHECK_PTR(ctx);
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);
libofx_proc_file(ctx, filename_deep, AUTODETECT);
libofx_free_context(ctx);
if ( m_valid )
{
m_fatalerror = TQString();
m_valid = storeStatements(m_statementlist);
}
return m_valid;
}
TQString OfxImporterPlugin::lastError(void) const
{
if(m_errors.count() == 0)
return m_fatalerror;
return m_errors.join("<p>");
}
/* __________________________________________________________________________
* AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*
* Static callbacks for LibOFX
*
* YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
*/
int OfxImporterPlugin::ofxTransactionCallback(struct OfxTransactionData data, void * pv)
{
// kdDebug(2) << __func__ << endl;
OfxImporterPlugin* pofx = reinterpret_cast<OfxImporterPlugin*>(pv);
MyMoneyStatement& s = pofx->back();
MyMoneyStatement::Transaction t;
if(data.date_posted_valid==true)
{
TQDateTime dt;
dt.setTime_t(data.date_posted, Qt::UTC);
t.m_datePosted = dt.date();
}
else if(data.date_initiated_valid==true)
{
TQDateTime dt;
dt.setTime_t(data.date_initiated, Qt::UTC);
t.m_datePosted = dt.date();
}
if(data.amount_valid==true)
{
t.m_amount = MyMoneyMoney(data.amount, 1000);
// if this is an investment statement, reverse the sign. not sure
// why this is needed, so I suppose it's a bit of a hack for the moment.
if (data.invtransactiontype_valid==true)
t.m_amount = -t.m_amount;
}
if(data.check_number_valid==true)
{
t.m_strNumber = data.check_number;
}
if(data.fi_id_valid==true)
{
t.m_strBankID = TQString("ID ") + data.fi_id;
}
else if(data.reference_number_valid==true)
{
t.m_strBankID = TQString("REF ") + data.reference_number;
}
// Decide whether to import NAME or PAYEEID if both are present in the download
if (pofx->m_preferName) {
if(data.name_valid==true)
{
t.m_strPayee = data.name;
}
else if(data.payee_id_valid==true)
{
t.m_strPayee = data.payee_id;
}
}
else {
if(data.payee_id_valid==true)
{
t.m_strPayee = data.payee_id;
}
else if(data.name_valid==true)
{
t.m_strPayee = data.name;
}
}
if(data.memo_valid==true){
t.m_strMemo = 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==true)
{
struct OfxSecurityData* secdata = data.security_data_ptr;
if(secdata->ticker_valid==true){
t.m_strSymbol = secdata->ticker;
}
if(secdata->secname_valid==true){
t.m_strSecurity = secdata->secname;
}
}
t.m_shares = MyMoneyMoney();
if(data.units_valid==true)
{
t.m_shares = MyMoneyMoney(data.units, 100000).reduce();
}
t.m_price = MyMoneyMoney();
if(data.unitprice_valid == true)
{
t.m_price = MyMoneyMoney(data.unitprice, 100000).reduce();
}
t.m_fees = MyMoneyMoney();
if(data.fees_valid==true)
{
t.m_fees += MyMoneyMoney(data.fees, 1000).reduce();
}
if(data.commission_valid==true)
{
t.m_fees += MyMoneyMoney(data.commission, 1000).reduce();
}
bool unhandledtype = false;
TQString type;
if(data.invtransactiontype_valid==true)
{
switch (data.invtransactiontype)
{
case OFX_BUYDEBT:
case OFX_BUYMF:
case OFX_BUYOPT:
case OFX_BUYOTHER:
case OFX_BUYSTOCK:
t.m_eAction = MyMoneyStatement::Transaction::eaBuy;
break;
case OFX_REINVEST:
t.m_eAction = MyMoneyStatement::Transaction::eaReinvestDividend;
break;
case OFX_SELLDEBT:
case OFX_SELLMF:
case OFX_SELLOPT:
case OFX_SELLOTHER:
case OFX_SELLSTOCK:
t.m_eAction = MyMoneyStatement::Transaction::eaSell;
break;
case OFX_INCOME:
t.m_eAction = MyMoneyStatement::Transaction::eaCashDividend;
// 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 = "CLOSUREOPT (Close a position for an option)";
break;
case OFX_INVEXPENSE:
unhandledtype = true;
type = "INVEXPENSE (Misc investment expense that is associated with a specific security)";
break;
case OFX_JRNLFUND:
unhandledtype = true;
type = "JRNLFUND (Journaling cash holdings between subaccounts within the same investment account)";
break;
case OFX_MARGININTEREST:
unhandledtype = true;
type = "MARGININTEREST (Margin interest expense)";
break;
case OFX_RETOFCAP:
unhandledtype = true;
type = "RETOFCAP (Return of capital)";
break;
case OFX_SPLIT:
unhandledtype = true;
type = "SPLIT (Stock or mutial fund split)";
break;
case OFX_TRANSFER:
unhandledtype = true;
type = "TRANSFER (Transfer holdings in and out of the investment account)";
break;
default:
unhandledtype = true;
type = TQString("UNKNOWN %1").arg(data.invtransactiontype);
break;
}
}
else
t.m_eAction = MyMoneyStatement::Transaction::eaNone;
// 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==true && data.unitprice_valid)
{
double proper_total = t.m_dShares * data.unitprice + t.m_moneyFees;
if ( proper_total != t.m_moneyAmount )
{
pofx->addWarning(TQString("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(TQString("Transaction %1 has an unsupported type (%2).").arg(t.m_strBankID,type));
else
s.m_listTransactions += t;
// kdDebug(2) << __func__ << "return 0 " << endl;
return 0;
}
int OfxImporterPlugin::ofxStatementCallback(struct OfxStatementData data, void* pv)
{
// kdDebug(2) << __func__ << endl;
OfxImporterPlugin* pofx = reinterpret_cast<OfxImporterPlugin*>(pv);
MyMoneyStatement& s = pofx->back();
pofx->setValid();
if(data.currency_valid==true)
{
s.m_strCurrency = data.currency;
}
if(data.account_id_valid==true)
{
s.m_strAccountNumber = data.account_id;
}
if(data.date_start_valid==true)
{
TQDateTime dt;
dt.setTime_t(data.date_start, Qt::UTC);
s.m_dateBegin = dt.date();
}
if(data.date_end_valid==true)
{
TQDateTime dt;
dt.setTime_t(data.date_end, Qt::UTC);
s.m_dateEnd = dt.date();
}
if(data.ledger_balance_valid==true)
{
s.m_closingBalance = MyMoneyMoney(data.ledger_balance);
}
// kdDebug(2) << __func__ << " return 0" << endl;
return 0;
}
int OfxImporterPlugin::ofxAccountCallback(struct OfxAccountData data, void * pv)
{
// kdDebug(2) << __func__ << endl;
OfxImporterPlugin* pofx = reinterpret_cast<OfxImporterPlugin*>(pv);
pofx->addnew();
MyMoneyStatement& s = pofx->back();
// Having any account at all makes an ofx statement valid
pofx->m_valid = true;
if(data.account_id_valid==true)
{
s.m_strAccountName = data.account_name;
s.m_strAccountNumber = data.account_id;
}
if(data.bank_id_valid == true)
{
s.m_strRoutingNumber = data.bank_id;
}
if(data.broker_id_valid == true)
{
s.m_strRoutingNumber = data.broker_id;
}
if(data.currency_valid==true)
{
s.m_strCurrency = data.currency;
}
if(data.account_type_valid==true)
{
switch(data.account_type)
{
case OfxAccountData::OFX_CHECKING : s.m_eType = MyMoneyStatement::etCheckings;
break;
case OfxAccountData::OFX_SAVINGS : s.m_eType = MyMoneyStatement::etSavings;
break;
case OfxAccountData::OFX_MONEYMRKT : s.m_eType = MyMoneyStatement::etInvestment;
break;
case OfxAccountData::OFX_CREDITLINE : s.m_eType = MyMoneyStatement::etCreditCard;
break;
case OfxAccountData::OFX_CMA : s.m_eType = MyMoneyStatement::etCreditCard;
break;
case OfxAccountData::OFX_CREDITCARD : s.m_eType = MyMoneyStatement::etCreditCard;
break;
case OfxAccountData::OFX_INVESTMENT : s.m_eType = MyMoneyStatement::etInvestment;
break;
}
}
// ask KMyMoney for an account id
s.m_accountId = pofx->account("kmmofx-acc-ref", TQString("%1-%2").arg(s.m_strRoutingNumber, s.m_strAccountNumber)).id();
// copy over the securities
s.m_listSecurities = pofx->m_securitylist;
// kdDebug(2) << __func__ << " return 0" << endl;
return 0;
}
int OfxImporterPlugin::ofxSecurityCallback(struct OfxSecurityData data, void* pv)
{
// kdDebug(2) << __func__ << endl;
OfxImporterPlugin* pofx = reinterpret_cast<OfxImporterPlugin*>(pv);
MyMoneyStatement::Security sec;
if(data.unique_id_valid==true){
sec.m_strId = data.unique_id;
}
if(data.secname_valid==true){
sec.m_strName = data.secname;
}
if(data.ticker_valid==true){
sec.m_strSymbol = data.ticker;
}
pofx->m_securitylist += sec;
return 0;
}
int OfxImporterPlugin::ofxStatusCallback(struct OfxStatusData data, void * pv)
{
// kdDebug(2) << __func__ << endl;
OfxImporterPlugin* pofx = reinterpret_cast<OfxImporterPlugin*>(pv);
TQString 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->m_fatalerror = "No accounts found.";
if(data.ofx_element_name_valid==true)
message.prepend(TQString("%1: ").arg(data.ofx_element_name));
if(data.code_valid==true)
message += TQString("%1 (Code %2): %3").arg(data.name).arg(data.code).arg(data.description);
if(data.server_message_valid==true)
message += TQString(" (%1)").arg(data.server_message);
if(data.severity_valid==true){
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( "Previous message was an unknown type. 'WARNING' was assumed.");
break;
}
}
// kdDebug(2) << __func__ << " return 0 " << endl;
return 0;
}
bool OfxImporterPlugin::importStatement(const MyMoneyStatement& s)
{
tqDebug("OfxImporterPlugin::importStatement start");
return statementInterface()->import(s);
}
const MyMoneyAccount& OfxImporterPlugin::account(const TQString& key, const TQString& value) const
{
return statementInterface()->account(key, value);
}
void OfxImporterPlugin::protocols(TQStringList& protocolList) const
{
protocolList.clear();
protocolList << "OFX";
}
TQWidget* OfxImporterPlugin::accountConfigTab(const MyMoneyAccount& acc, TQString& name)
{
name = i18n("Online settings");
m_statusDlg = new KOnlineBankingStatus(acc, 0, 0);
return m_statusDlg;
}
MyMoneyKeyValueContainer OfxImporterPlugin::onlineBankingSettings(const MyMoneyKeyValueContainer& current)
{
MyMoneyKeyValueContainer kvp(current);
// keep the provider name in sync with the one found in kmm_ofximport.desktop
kvp["provider"] = "KMyMoney OFX";
if(m_statusDlg) {
kvp.deletePair("appId");
kvp.deletePair("kmmofx-headerVersion");
if(!m_statusDlg->appId().isEmpty())
kvp.setValue("appId", m_statusDlg->appId());
kvp.setValue("kmmofx-headerVersion", m_statusDlg->headerVersion());
kvp.setValue("kmmofx-numRequestDays", TQString::number(m_statusDlg->m_numdaysSpin->value()));
kvp.setValue("kmmofx-todayMinus", TQString::number(m_statusDlg->m_todayRB->isChecked()));
kvp.setValue("kmmofx-lastUpdate", TQString::number(m_statusDlg->m_lastUpdateRB->isChecked()));
kvp.setValue("kmmofx-pickDate", TQString::number(m_statusDlg->m_pickDateRB->isChecked()));
kvp.setValue("kmmofx-specificDate", m_statusDlg->m_specificDate->date().toString());
kvp.setValue("kmmofx-preferPayeeid", TQString::number(m_statusDlg->m_payeeidRB->isChecked()));
kvp.setValue("kmmofx-preferName", TQString::number(m_statusDlg->m_nameRB->isChecked()));
}
return kvp;
}
bool OfxImporterPlugin::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings)
{
Q_UNUSED(acc);
bool rc = false;
KOnlineBankingSetupWizard wiz(0, "onlinebankingsetup");
if(wiz.isInit()) {
if(wiz.exec() == TQDialog::Accepted) {
rc = wiz.chosenSettings( settings );
}
}
return rc;
}
bool OfxImporterPlugin::updateAccount(const MyMoneyAccount& acc, bool moreAccounts)
{
Q_UNUSED(moreAccounts);
try {
if(!acc.id().isEmpty()) {
// Save the value of preferName to be used by ofxTransactionCallback
m_preferName = acc.onlineBankingSettings().value("kmmofx-preferName").toInt() != 0;
KOfxDirectConnectDlg dlg(acc);
connect(&dlg, TQT_SIGNAL(statementReady(const TQString&)),
this, TQT_SLOT(slotImportFile(const TQString&)));
dlg.init();
dlg.exec();
}
} catch (MyMoneyException *e) {
KMessageBox::information(0 ,i18n("Error connecting to bank: %1").arg(e->what()));
delete e;
}
return false;
}
void OfxImporterPlugin::slotImportFile(const TQString& url)
{
if(!import(url)) {
KMessageBox::error( 0, TQString("<qt>%1</qt>").arg(i18n("Unable to import %1 using the OFX importer plugin. The plugin returned the following error:<p>%2").arg(url, lastError())), i18n("Importing error"));
}
}
bool OfxImporterPlugin::storeStatements(TQValueList<MyMoneyStatement>& statements)
{
bool hasstatements = (statements.count() > 0);
bool ok = true;
bool 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"),KStdGuiItem::cont(),"ofxwarnings") == KMessageBox::Cancel )
abort = true;
}*/
tqDebug("OfxImporterPlugin::storeStatements() with %d statements called", static_cast<int>(statements.count()));
TQValueList<MyMoneyStatement>::const_iterator it_s = statements.begin();
while ( it_s != statements.end() && !abort ) {
ok = ok && importStatement((*it_s));
++it_s;
}
if ( hasstatements && !ok ) {
KMessageBox::error( 0, i18n("Importing process terminated unexpectedly."), i18n("Failed to import all statements."));
}
return ( !hasstatements || ok );
}
#include "ofximporterplugin.moc"
// vim:cin:si:ai:et:ts=2:sw=2: