ofximporter.cpp 26.6 KB
Newer Older
Alvaro Soliverez's avatar
Alvaro Soliverez committed
1
/***************************************************************************
2
                          ofximporter.cpp
Alvaro Soliverez's avatar
Alvaro Soliverez committed
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                             -------------------
    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.                                   *
 *                                                                         *
 ***************************************************************************/

18
#include "ofximporter.h"
Cristian Oneț's avatar
Cristian Oneț committed
19

Alvaro Soliverez's avatar
Alvaro Soliverez committed
20
21
22
// ----------------------------------------------------------------------------
// QT Includes

Nicolas Lécureuil's avatar
Nicolas Lécureuil committed
23
#include <QFile>
24
#include <QTextStream>
Nicolas Lécureuil's avatar
Nicolas Lécureuil committed
25
#include <QRadioButton>
26
#include <QSpinBox>
Nicolas Lécureuil's avatar
Nicolas Lécureuil committed
27
#include <QByteArray>
Alvaro Soliverez's avatar
Alvaro Soliverez committed
28
29
30
31

// ----------------------------------------------------------------------------
// KDE Includes

32
#include <KPluginFactory>
Cristian Oneț's avatar
Cristian Oneț committed
33
#include <QUrl>
34
35
#include <KMessageBox>
#include <KActionCollection>
36
#include <KLocalizedString>
37
#include <KWallet>
Alvaro Soliverez's avatar
Alvaro Soliverez committed
38
39
40
41

// ----------------------------------------------------------------------------
// Project Includes

Łukasz Wojniłowicz's avatar
Łukasz Wojniłowicz committed
42
#include <libofx/libofx.h>
Alvaro Soliverez's avatar
Alvaro Soliverez committed
43
44
45
#include "konlinebankingstatus.h"
#include "konlinebankingsetupwizard.h"
#include "kofxdirectconnectdlg.h"
46
#include "mymoneyaccount.h"
Łukasz Wojniłowicz's avatar
Łukasz Wojniłowicz committed
47
#include "mymoneyexception.h"
Łukasz Wojniłowicz's avatar
Łukasz Wojniłowicz committed
48
49
50
#include "mymoneystatement.h"
#include "statementinterface.h"
#include "importinterface.h"
51
#include "ui_importoption.h"
Alvaro Soliverez's avatar
Alvaro Soliverez committed
52

53
54
//#define DEBUG_LIBOFX

55
56
using KWallet::Wallet;

57
class OFXImporter::Private
58
59
{
public:
60
61
  Private() : m_valid(false), m_preferName(PreferId), m_walletIsOpen(false), m_statusDlg(0), m_wallet(0),
              m_updateStartDate(QDate(1900,1,1)) {}
62
63

  bool m_valid;
64
65
66
67
68
  enum NamePreference {
    PreferId = 0,
    PreferName,
    PreferMemo
  } m_preferName;
69
70
71
72
73
74
75
76
  bool m_walletIsOpen;
  QList<MyMoneyStatement> m_statementlist;
  QList<MyMoneyStatement::Security> m_securitylist;
  QString m_fatalerror;
  QStringList m_infos;
  QStringList m_warnings;
  QStringList m_errors;
  KOnlineBankingStatus* m_statusDlg;
77
  Wallet *m_wallet;
78
  QDate m_updateStartDate;
79
80
81
82
83
};




84
85
OFXImporter::OFXImporter(QObject *parent, const QVariantList &args) :
    KMyMoneyPlugin::Plugin(parent, "ofximporter"),
Thomas Baumgart's avatar
Thomas Baumgart committed
86
87
88
89
90
    /*
     * the string in the line above must be the same as
     * X-KDE-PluginInfo-Name and the provider name assigned in
     * OfxImporterPlugin::onlineBankingSettings()
     */
91
    KMyMoneyPlugin::ImporterPlugin(),
92
    d(new Private)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
93
{
94
95
96
  Q_UNUSED(args)
  setComponentName("ofximporter", i18n("OFX Importer"));
  setXMLFile("ofximporter.rc");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
97
  createActions();
98
99

  // For ease announce that we have been loaded.
100
  qDebug("Plugins: ofximporter loaded");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
101
102
}

103
OFXImporter::~OFXImporter()
Alvaro Soliverez's avatar
Alvaro Soliverez committed
104
{
105
  delete d;
106
  qDebug("Plugins: ofximporter unloaded");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
107
108
}

109
void OFXImporter::createActions()
Alvaro Soliverez's avatar
Alvaro Soliverez committed
110
{
111
  QAction *action = actionCollection()->addAction("file_import_ofx");
112
113
  action->setText(i18n("OFX..."));
  connect(action, SIGNAL(triggered(bool)), this, SLOT(slotImportFile()));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
114
115
}

116
void OFXImporter::slotImportFile()
Alvaro Soliverez's avatar
Alvaro Soliverez committed
117
{
118
119
120
121
  QWidget * widget = new QWidget;
  Ui_ImportOption* option = new Ui_ImportOption;
  option->setupUi(widget);

Cristian Oneț's avatar
Cristian Oneț committed
122
  QUrl url = importInterface()->selectFile(i18n("OFX import file selection"),
123
             "",
Thomas Baumgart's avatar
Thomas Baumgart committed
124
             "*.ofx *.qfx *.ofc|OFX files (*.ofx *.qfx *.ofc);;*|All files (*)",
125
             QFileDialog::ExistingFile,
126
127
             widget);

128
  d->m_preferName = static_cast<OFXImporter::Private::NamePreference>(option->m_preferName->currentIndex());
129

130
131
  if (url.isValid()) {
    if (isMyFormat(url.path())) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
132
133
      slotImportFile(url.path());
    } else {
Cristian Oneț's avatar
Cristian Oneț committed
134
      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"));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
135
136
137
    }

  }
138
  delete widget;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
139
140
}

141
QString OFXImporter::formatName() const
Alvaro Soliverez's avatar
Alvaro Soliverez committed
142
143
144
145
{
  return "OFX";
}

146
QString OFXImporter::formatFilenameFilter() const
Alvaro Soliverez's avatar
Alvaro Soliverez committed
147
148
149
150
151
{
  return "*.ofx *.qfx *.ofc";
}


152
bool OFXImporter::isMyFormat(const QString& filename) const
Alvaro Soliverez's avatar
Alvaro Soliverez committed
153
154
155
{
  // filename is considered an Ofx file if it contains
  // the tag "<OFX>" or "<OFC>" in the first 20 lines.
156
  // which contain some data
Alvaro Soliverez's avatar
Alvaro Soliverez committed
157
158
  bool result = false;

159
  QFile f(filename);
160
161
  if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
    QTextStream ts(&f);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
162
163

    int lineCount = 20;
164
    while (!ts.atEnd() && !result  && lineCount != 0) {
165
166
      // get a line of data and remove all unnecessary whitepace chars
      QString line = ts.readLine().simplified();
Thomas Baumgart's avatar
Thomas Baumgart committed
167
168
      if (line.contains("<OFX>", Qt::CaseInsensitive)
          || line.contains("<OFC>", Qt::CaseInsensitive))
Alvaro Soliverez's avatar
Alvaro Soliverez committed
169
        result = true;
170
      // count only lines that contain some non white space chars
Thomas Baumgart's avatar
Thomas Baumgart committed
171
      if (!line.isEmpty())
172
        lineCount--;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
173
174
175
176
177
178
179
    }
    f.close();
  }

  return result;
}

180
bool OFXImporter::import(const QString& filename)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
181
{
182
183
184
185
186
  d->m_fatalerror = i18n("Unable to parse file");
  d->m_valid = false;
  d->m_errors.clear();
  d->m_warnings.clear();
  d->m_infos.clear();
Alvaro Soliverez's avatar
Alvaro Soliverez committed
187

188
189
  d->m_statementlist.clear();
  d->m_securitylist.clear();
Alvaro Soliverez's avatar
Alvaro Soliverez committed
190

191
  QByteArray filename_deep = QFile::encodeName(filename);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
192

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
  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

Alvaro Soliverez's avatar
Alvaro Soliverez committed
208
209
210
  LibofxContextPtr ctx = libofx_get_new_context();
  Q_CHECK_PTR(ctx);

211
  qDebug("setup callback routines");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
212
213
214
215
216
  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);
217
  qDebug("process data");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
218
219
220
  libofx_proc_file(ctx, filename_deep, AUTODETECT);
  libofx_free_context(ctx);

221
222
223
  if (d->m_valid) {
    d->m_fatalerror.clear();
    d->m_valid = storeStatements(d->m_statementlist);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
224
  }
225
  return d->m_valid;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
226
227
}

228
QString OFXImporter::lastError() const
Alvaro Soliverez's avatar
Alvaro Soliverez committed
229
{
230
231
232
  if (d->m_errors.count() == 0)
    return d->m_fatalerror;
  return d->m_errors.join("<p>");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
233
234
235
236
237
238
239
240
241
242
}

/* __________________________________________________________________________
 * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 *
 * Static callbacks for LibOFX
 *
 * YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
 */

243
int OFXImporter::ofxTransactionCallback(struct OfxTransactionData data, void * pv)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
244
{
245
//   kDebug(2) << Q_FUNC_INFO;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
246

247
  OFXImporter* pofx = reinterpret_cast<OFXImporter*>(pv);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
248
249
250
251
  MyMoneyStatement& s = pofx->back();

  MyMoneyStatement::Transaction t;

252
  if (data.date_posted_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
253
    QDateTime dt;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
254
    dt.setTime_t(data.date_posted);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
255
    t.m_datePosted = dt.date();
256
  } else if (data.date_initiated_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
257
    QDateTime dt;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
258
    dt.setTime_t(data.date_initiated);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
259
260
    t.m_datePosted = dt.date();
  }
261
262
263
264
265
266
267
  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;
    }
  }
Alvaro Soliverez's avatar
Alvaro Soliverez committed
268

269
  if (data.amount_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
270
271
272
    t.m_amount = MyMoneyMoney(data.amount, 1000);
  }

273
  if (data.check_number_valid) {
274
    t.m_strNumber = QString::fromUtf8(data.check_number);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
275
276
  }

277
  if (data.fi_id_valid) {
278
    t.m_strBankID = QString("ID ") + QString::fromUtf8(data.fi_id);
279
  } else if (data.reference_number_valid) {
280
    t.m_strBankID = QString("REF ") + QString::fromUtf8(data.reference_number);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
281
  }
282
283
284
285
286

  // 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) {
287
    case OFXImporter::Private::PreferId:  // PAYEEID
288
289
290
291
    default:
      validity[0] = data.payee_id_valid;
      validity[1] = data.name_valid;
      validity[2] = data.memo_valid;
292
293
294
      values += QString::fromUtf8(data.payee_id);
      values += QString::fromUtf8(data.name);
      values += QString::fromUtf8(data.memo);
295
296
      break;

297
    case OFXImporter::Private::PreferName:  // NAME
298
299
300
      validity[0] = data.name_valid;
      validity[1] = data.payee_id_valid;
      validity[2] = data.memo_valid;
301
302
303
      values += QString::fromUtf8(data.name);
      values += QString::fromUtf8(data.payee_id);
      values += QString::fromUtf8(data.memo);
304
305
      break;

306
    case OFXImporter::Private::PreferMemo:  // MEMO
307
      validity[0] = data.memo_valid;
308
309
      validity[1] = data.payee_id_valid;
      validity[2] = data.name_valid;
310
311
312
      values += QString::fromUtf8(data.memo);
      values += QString::fromUtf8(data.payee_id);
      values += QString::fromUtf8(data.name);
313
314
315
      break;
  }

316
317
318
319
320
321
322
323
324
325
  // 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);
  }

326
327
328
329
  for (int idx = 0; idx < 3; ++idx) {
    if (validity[idx]) {
      t.m_strPayee = values[idx];
      break;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
330
331
    }
  }
332
333

  // extract memo field if we haven't used it as payee
334
  if ((data.memo_valid)
335
      && (pofx->d->m_preferName != OFXImporter::Private::PreferMemo)) {
336
    t.m_strMemo = QString::fromUtf8(data.memo);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
337
338
339
340
  }

  // If the payee or memo fields are blank, set them to
  // the other one which is NOT blank.  (acejones)
341
  if (t.m_strPayee.isEmpty()) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
342
    // But we only create a payee for non-investment transactions (ipwizard)
343
    if (! t.m_strMemo.isEmpty() && data.invtransactiontype_valid == false)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
344
      t.m_strPayee = t.m_strMemo;
345
346
  } else {
    if (t.m_strMemo.isEmpty())
Alvaro Soliverez's avatar
Alvaro Soliverez committed
347
348
349
      t.m_strMemo = t.m_strPayee;
  }

350
  if (data.security_data_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
351
352
    struct OfxSecurityData* secdata = data.security_data_ptr;

353
    if (secdata->ticker_valid) {
354
      t.m_strSymbol = QString::fromUtf8(secdata->ticker);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
355
356
    }

357
    if (secdata->secname_valid) {
358
      t.m_strSecurity = QString::fromUtf8(secdata->secname);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
359
360
361
362
    }
  }

  t.m_shares = MyMoneyMoney();
363
  if (data.units_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
364
365
366
367
    t.m_shares = MyMoneyMoney(data.units, 100000).reduce();
  }

  t.m_price = MyMoneyMoney();
368
  if (data.unitprice_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
369
370
371
372
    t.m_price = MyMoneyMoney(data.unitprice, 100000).reduce();
  }

  t.m_fees = MyMoneyMoney();
373
  if (data.fees_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
374
375
376
    t.m_fees += MyMoneyMoney(data.fees, 1000).reduce();
  }

377
  if (data.commission_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
378
379
380
381
382
383
    t.m_fees += MyMoneyMoney(data.commission, 1000).reduce();
  }

  bool unhandledtype = false;
  QString type;

384
  if (data.invtransactiontype_valid) {
385
    switch (data.invtransactiontype) {
Ian Neal's avatar
Ian Neal committed
386
387
388
389
390
      case OFX_BUYDEBT:
      case OFX_BUYMF:
      case OFX_BUYOPT:
      case OFX_BUYOTHER:
      case OFX_BUYSTOCK:
391
        t.m_eAction = eMyMoney::Transaction::Action::Buy;
Ian Neal's avatar
Ian Neal committed
392
393
        break;
      case OFX_REINVEST:
394
        t.m_eAction = eMyMoney::Transaction::Action::ReinvestDividend;
Ian Neal's avatar
Ian Neal committed
395
396
397
398
399
400
        break;
      case OFX_SELLDEBT:
      case OFX_SELLMF:
      case OFX_SELLOPT:
      case OFX_SELLOTHER:
      case OFX_SELLSTOCK:
401
        t.m_eAction = eMyMoney::Transaction::Action::Sell;
Ian Neal's avatar
Ian Neal committed
402
403
        break;
      case OFX_INCOME:
404
        t.m_eAction = eMyMoney::Transaction::Action::CashDividend;
Ian Neal's avatar
Ian Neal committed
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
        // 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 = QString("UNKNOWN %1").arg(data.invtransactiontype);
        break;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
445
    }
446
  } else
447
    t.m_eAction = eMyMoney::Transaction::Action::None;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
448
449
450
451
452
453
454
455
456
457
458

  // 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.

459
  if (data.invtransactiontype_valid && data.unitprice_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
460
    double proper_total = t.m_dShares * data.unitprice + t.m_moneyFees;
461
    if (proper_total != t.m_moneyAmount) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
462
463
464
465
466
467
      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

468
469
  if (unhandledtype)
    pofx->addWarning(QString("Transaction %1 has an unsupported type (%2).").arg(t.m_strBankID, type));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
470
471
472
  else
    s.m_listTransactions += t;

473
//   kDebug(2) << Q_FUNC_INFO << "return 0 ";
Alvaro Soliverez's avatar
Alvaro Soliverez committed
474
475
476
477

  return 0;
}

478
int OFXImporter::ofxStatementCallback(struct OfxStatementData data, void* pv)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
479
{
480
//   kDebug(2) << Q_FUNC_INFO;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
481

482
  OFXImporter* pofx = reinterpret_cast<OFXImporter*>(pv);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
483
484
485
486
  MyMoneyStatement& s = pofx->back();

  pofx->setValid();

487
  if (data.currency_valid) {
488
    s.m_strCurrency = QString::fromUtf8(data.currency);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
489
  }
490
  if (data.account_id_valid) {
491
    s.m_strAccountNumber = QString::fromUtf8(data.account_id);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
492
493
  }

494
  if (data.date_start_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
495
    QDateTime dt;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
496
    dt.setTime_t(data.date_start);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
497
498
499
    s.m_dateBegin = dt.date();
  }

500
  if (data.date_end_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
501
    QDateTime dt;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
502
    dt.setTime_t(data.date_end);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
503
504
505
    s.m_dateEnd = dt.date();
  }

506
  if (data.ledger_balance_valid && data.ledger_balance_date_valid) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
507
    s.m_closingBalance = MyMoneyMoney(data.ledger_balance);
508
509
510
    QDateTime dt;
    dt.setTime_t(data.ledger_balance_date);
    s.m_dateEnd = dt.date();
Alvaro Soliverez's avatar
Alvaro Soliverez committed
511
512
  }

513
//   kDebug(2) << Q_FUNC_INFO << " return 0";
Alvaro Soliverez's avatar
Alvaro Soliverez committed
514
515
516
517

  return 0;
}

518
int OFXImporter::ofxAccountCallback(struct OfxAccountData data, void * pv)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
519
{
520
//   kDebug(2) << Q_FUNC_INFO;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
521

522
  OFXImporter* pofx = reinterpret_cast<OFXImporter*>(pv);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
523
524
525
526
  pofx->addnew();
  MyMoneyStatement& s = pofx->back();

  // Having any account at all makes an ofx statement valid
527
  pofx->d->m_valid = true;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
528

529
  if (data.account_id_valid) {
530
531
    s.m_strAccountName = QString::fromUtf8(data.account_name);
    s.m_strAccountNumber = QString::fromUtf8(data.account_id);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
532
  }
533
  if (data.bank_id_valid) {
534
    s.m_strRoutingNumber = QString::fromUtf8(data.bank_id);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
535
  }
536
  if (data.broker_id_valid) {
537
    s.m_strRoutingNumber = QString::fromUtf8(data.broker_id);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
538
  }
539
  if (data.currency_valid) {
540
    s.m_strCurrency = QString::fromUtf8(data.currency);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
541
542
  }

543
  if (data.account_type_valid) {
544
    switch (data.account_type) {
545
      case OfxAccountData::OFX_CHECKING : s.m_eType = eMyMoney::Statement::Type::Checkings;
Ian Neal's avatar
Ian Neal committed
546
        break;
547
      case OfxAccountData::OFX_SAVINGS : s.m_eType = eMyMoney::Statement::Type::Savings;
Ian Neal's avatar
Ian Neal committed
548
        break;
549
      case OfxAccountData::OFX_MONEYMRKT : s.m_eType = eMyMoney::Statement::Type::Investment;
Ian Neal's avatar
Ian Neal committed
550
        break;
551
      case OfxAccountData::OFX_CREDITLINE : s.m_eType = eMyMoney::Statement::Type::CreditCard;
Ian Neal's avatar
Ian Neal committed
552
        break;
553
      case OfxAccountData::OFX_CMA : s.m_eType = eMyMoney::Statement::Type::CreditCard;
Ian Neal's avatar
Ian Neal committed
554
        break;
555
      case OfxAccountData::OFX_CREDITCARD : s.m_eType = eMyMoney::Statement::Type::CreditCard;
Ian Neal's avatar
Ian Neal committed
556
        break;
557
      case OfxAccountData::OFX_INVESTMENT : s.m_eType = eMyMoney::Statement::Type::Investment;
Ian Neal's avatar
Ian Neal committed
558
        break;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
559
560
561
562
563
564
565
    }
  }

  // ask KMyMoney for an account id
  s.m_accountId = pofx->account("kmmofx-acc-ref", QString("%1-%2").arg(s.m_strRoutingNumber, s.m_strAccountNumber)).id();

  // copy over the securities
566
  s.m_listSecurities = pofx->d->m_securitylist;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
567

568
//   kDebug(2) << Q_FUNC_INFO << " return 0";
Alvaro Soliverez's avatar
Alvaro Soliverez committed
569
570
571
572

  return 0;
}

573
int OFXImporter::ofxSecurityCallback(struct OfxSecurityData data, void* pv)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
574
{
575
  //   kDebug(2) << Q_FUNC_INFO;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
576

577
  OFXImporter* pofx = reinterpret_cast<OFXImporter*>(pv);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
578
579
  MyMoneyStatement::Security sec;

580
  if (data.unique_id_valid) {
581
    sec.m_strId = QString::fromUtf8(data.unique_id);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
582
  }
583
  if (data.secname_valid) {
584
    sec.m_strName = QString::fromUtf8(data.secname);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
585
  }
586
  if (data.ticker_valid) {
587
    sec.m_strSymbol = QString::fromUtf8(data.ticker);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
588
589
  }

590
  pofx->d->m_securitylist += sec;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
591
592
593
594

  return 0;
}

595
int OFXImporter::ofxStatusCallback(struct OfxStatusData data, void * pv)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
596
{
597
//   kDebug(2) << Q_FUNC_INFO;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
598

599
  OFXImporter* pofx = reinterpret_cast<OFXImporter*>(pv);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
600
601
602
603
604
  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!
Laurent Montel's avatar
Laurent Montel committed
605
  pofx->d->m_fatalerror = i18n("No accounts found.");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
606

607
  if (data.ofx_element_name_valid)
608
    message.prepend(QString("%1: ").arg(QString::fromUtf8(data.ofx_element_name)));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
609

610
  if (data.code_valid)
611
    message += QString("%1 (Code %2): %3").arg(QString::fromUtf8(data.name)).arg(data.code).arg(QString::fromUtf8(data.description));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
612

613
  if (data.server_message_valid)
614
    message += QString(" (%1)").arg(QString::fromUtf8(data.server_message));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
615

616
  if (data.severity_valid) {
617
    switch (data.severity) {
Ian Neal's avatar
Ian Neal committed
618
619
620
621
622
623
624
625
626
627
628
629
630
      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;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
631
632
633
    }
  }

634
//   kDebug(2) << Q_FUNC_INFO << " return 0 ";
Alvaro Soliverez's avatar
Alvaro Soliverez committed
635
636
637
638

  return 0;
}

639
bool OFXImporter::importStatement(const MyMoneyStatement& s)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
640
641
642
643
644
{
  qDebug("OfxImporterPlugin::importStatement start");
  return statementInterface()->import(s);
}

645
MyMoneyAccount OFXImporter::account(const QString& key, const QString& value) const
Alvaro Soliverez's avatar
Alvaro Soliverez committed
646
647
648
649
{
  return statementInterface()->account(key, value);
}

650
void OFXImporter::protocols(QStringList& protocolList) const
Alvaro Soliverez's avatar
Alvaro Soliverez committed
651
652
653
654
655
{
  protocolList.clear();
  protocolList << "OFX";
}

656
QWidget* OFXImporter::accountConfigTab(const MyMoneyAccount& acc, QString& name)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
657
658
{
  name = i18n("Online settings");
659
660
  d->m_statusDlg = new KOnlineBankingStatus(acc, 0);
  return d->m_statusDlg;
Alvaro Soliverez's avatar
Alvaro Soliverez committed
661
662
}

663
MyMoneyKeyValueContainer OFXImporter::onlineBankingSettings(const MyMoneyKeyValueContainer& current)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
664
665
666
{
  MyMoneyKeyValueContainer kvp(current);
  // keep the provider name in sync with the one found in kmm_ofximport.desktop
667
  kvp["provider"] = QLatin1String("OFX Importer");
668
  if (d->m_statusDlg) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
669
670
    kvp.deletePair("appId");
    kvp.deletePair("kmmofx-headerVersion");
671
    kvp.deletePair("password");
672

673
    d->m_wallet = openSynchronousWallet();
674
675
676
677
678
679
680
681
682
683
684
    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("url"), kvp.value("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);
        }
      }
685
686
687
688
    } else {
      if (d->m_statusDlg->m_storePassword->isChecked()) {
        kvp.setValue("password", d->m_statusDlg->m_password->text());
      }
689
690
691
692
693
694
695
696
697
698
    }

    if (!d->m_statusDlg->appId().isEmpty())
      kvp.setValue("appId", d->m_statusDlg->appId());
    kvp.setValue("kmmofx-headerVersion", d->m_statusDlg->headerVersion());
    kvp.setValue("kmmofx-numRequestDays", QString::number(d->m_statusDlg->m_numdaysSpin->value()));
    kvp.setValue("kmmofx-todayMinus", QString::number(d->m_statusDlg->m_todayRB->isChecked()));
    kvp.setValue("kmmofx-lastUpdate", QString::number(d->m_statusDlg->m_lastUpdateRB->isChecked()));
    kvp.setValue("kmmofx-pickDate", QString::number(d->m_statusDlg->m_pickDateRB->isChecked()));
    kvp.setValue("kmmofx-specificDate", d->m_statusDlg->m_specificDate->date().toString());
699
    kvp.setValue("kmmofx-preferName", QString::number(d->m_statusDlg->m_preferredPayee->currentIndex()));
700
701
702
703
704
705
    if (!d->m_statusDlg->m_clientUidEdit->text().isEmpty())
      kvp.setValue("clientUid", d->m_statusDlg->m_clientUidEdit->text());
    else
      kvp.deletePair("clientUid");
    // get rid of pre 4.6 values
    kvp.deletePair("kmmofx-preferPayeeid");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
706
707
708
709
  }
  return kvp;
}

710
bool OFXImporter::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
711
712
713
714
{
  Q_UNUSED(acc);

  bool rc = false;
715
  QPointer<KOnlineBankingSetupWizard> wiz = new KOnlineBankingSetupWizard(0);
716
717
718
  if (wiz->isInit()) {
    if (wiz->exec() == QDialog::Accepted) {
      rc = wiz->chosenSettings(settings);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
719
720
721
    }
  }

722
723
  delete wiz;

Alvaro Soliverez's avatar
Alvaro Soliverez committed
724
725
726
  return rc;
}

727
bool OFXImporter::updateAccount(const MyMoneyAccount& acc, bool moreAccounts)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
728
729
730
{
  Q_UNUSED(moreAccounts);

731
  qDebug("OfxImporterPlugin::updateAccount");
Alvaro Soliverez's avatar
Alvaro Soliverez committed
732
  try {
733
    if (!acc.id().isEmpty()) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
734
      // Save the value of preferName to be used by ofxTransactionCallback
735
      d->m_preferName = static_cast<OFXImporter::Private::NamePreference>(acc.onlineBankingSettings().value("kmmofx-preferName").toInt());
736
      QPointer<KOfxDirectConnectDlg> dlg = new KOfxDirectConnectDlg(acc);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
737

Laurent Montel's avatar
Laurent Montel committed
738
739
      connect(dlg, SIGNAL(statementReady(QString)),
              this, SLOT(slotImportFile(QString)));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
740

Yuri Chornoivan's avatar
Yuri Chornoivan committed
741
      // get the date of the earliest transaction that we are interested in
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
      // from the settings for this account
      MyMoneyKeyValueContainer settings = acc.onlineBankingSettings();
      if (!settings.value("provider").isEmpty()) {
        if ((settings.value("kmmofx-todayMinus").toInt() != 0) && !settings.value("kmmofx-numRequestDays").isEmpty()) {
          //kDebug(0) << "start date = today minus";
          d->m_updateStartDate = QDate::currentDate().addDays(-settings.value("kmmofx-numRequestDays").toInt());
        } else if ((settings.value("kmmofx-lastUpdate").toInt() != 0) && !acc.value("lastImportedTransactionDate").isEmpty()) {
          //kDebug(0) << "start date = last update";
          d->m_updateStartDate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate);
        } else if ((settings.value("kmmofx-pickDate").toInt() != 0) && !settings.value("kmmofx-specificDate").isEmpty()) {
          //kDebug(0) << "start date = pick date";
          d->m_updateStartDate = QDate::fromString(settings.value("kmmofx-specificDate"));
        }
        else {
          //kDebug(0) << "start date = today - 2 months";
          d->m_updateStartDate = QDate::currentDate().addMonths(-2);
        }
      }
      //kDebug(0) << "ofx plugin: account" << acc.name() << "earliest transaction date to process =" << qPrintable(d->m_updateStartDate.toString(Qt::ISODate));

762
763
      if (dlg->init())
        dlg->exec();
764
      delete dlg;
765
766
767
768

      // reset the earliest-interesting-transaction date to the non-specific account setting
      d->m_updateStartDate = QDate(1900,1,1);

Alvaro Soliverez's avatar
Alvaro Soliverez committed
769
    }
770
771
  } catch (const MyMoneyException &e) {
    KMessageBox::information(0 , i18n("Error connecting to bank: %1", e.what()));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
772
773
774
775
776
  }

  return false;
}

777
void OFXImporter::slotImportFile(const QString& url)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
778
{
779
  qDebug("OfxImporterPlugin::slotImportFile");
780
  if (!import(url)) {
781
    KMessageBox::error(0, QString("<qt>%1</qt>").arg(i18n("<p>Unable to import <b>'%1'</b> using the OFX importer plugin.  The plugin returned the following error:</p><p>%2</p>", url, lastError())), i18n("Importing error"));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
782
783
784
  }
}

785
bool OFXImporter::storeStatements(QList<MyMoneyStatement>& statements)
Alvaro Soliverez's avatar
Alvaro Soliverez committed
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
{
  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"),KStandardGuiItem::cont(),"ofxwarnings") == KMessageBox::Cancel )
      abort = true;
  }*/

  qDebug("OfxImporterPlugin::storeStatements() with %d statements called", static_cast<int>(statements.count()));
Cristian Oneț's avatar
Cristian Oneț committed
805
  QList<MyMoneyStatement>::const_iterator it_s = statements.constBegin();
806
  while (it_s != statements.constEnd() && !abort) {
Alvaro Soliverez's avatar
Alvaro Soliverez committed
807
808
809
810
    ok = ok && importStatement((*it_s));
    ++it_s;
  }

811
812
  if (hasstatements && !ok) {
    KMessageBox::error(0, i18n("Importing process terminated unexpectedly."), i18n("Failed to import all statements."));
Alvaro Soliverez's avatar
Alvaro Soliverez committed
813
814
  }

815
  return (!hasstatements || ok);
Alvaro Soliverez's avatar
Alvaro Soliverez committed
816
817
}

818
void OFXImporter::addnew()
819
820
821
{
  d->m_statementlist.push_back(MyMoneyStatement());
}
822
MyMoneyStatement& OFXImporter::back()
823
824
825
{
  return d->m_statementlist.back();
}
826
bool OFXImporter::isValid() const
827
828
829
{
  return d->m_valid;
}
830
void OFXImporter::setValid()
831
832
833
{
  d->m_valid = true;
}
834
void OFXImporter::addInfo(const QString& _msg)
835
836
837
{
  d->m_infos += _msg;
}
838
void OFXImporter::addWarning(const QString& _msg)
839
840
841
{
  d->m_warnings += _msg;
}
842
void OFXImporter::addError(const QString& _msg)
843
844
845
{
  d->m_errors += _msg;
}
846
const QStringList& OFXImporter::infos() const          // krazy:exclude=spelling
847
848
849
{
  return d->m_infos;
}
850
const QStringList& OFXImporter::warnings() const
851
852
853
{
  return d->m_warnings;
}
854
const QStringList& OFXImporter::errors() const
855
856
857
{
  return d->m_errors;
}
858
859
860
861

K_PLUGIN_FACTORY_WITH_JSON(OFXImporterFactory, "ofximporter.json", registerPlugin<OFXImporter>();)

#include "ofximporter.moc"