1 /*
2  * Copyright 2004       Martin Preuss aquamaniac@users.sourceforge.net
3  * Copyright 2009       Cristian Onet onet.cristian@gmail.com
4  * Copyright 2010-2019  Thomas Baumgart tbaumgart@kde.org
5  * Copyright 2015       Christian David christian-david@web.de
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation; either version 2 of
10  * the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include <config-kmymoney.h>
22 
23 #include "kbanking.h"
24 
25 #include <memory>
26 
27 // ----------------------------------------------------------------------------
28 // QT Includes
29 
30 #include <QLayout>
31 #include <QRadioButton>
32 #include <QStringList>
33 #include <QRegExp>
34 #include <QCheckBox>
35 #include <QLabel>
36 #include <QTimer>
37 #include <QRegularExpression>
38 
39 #include <QDebug> //! @todo remove @c #include <QDebug>
40 
41 #ifdef IS_APPIMAGE
42   #include <QCoreApplication>
43   #include <QStandardPaths>
44 #endif
45 
46 // ----------------------------------------------------------------------------
47 // KDE Includes
48 
49 #include <KPluginFactory>
50 #include <KLocalizedString>
51 #include <KMessageBox>
52 #include <KActionCollection>
53 #include <QMenu>
54 #include <KGuiItem>
55 #include <KLineEdit>
56 #include <KComboBox>
57 #include <KConfig>
58 #include <KConfigGroup>
59 #include <KAboutData>
60 
61 // ----------------------------------------------------------------------------
62 // Library Includes
63 
64 #include <aqbanking/banking.h>
65 #include <aqbanking/types/imexporter_context.h>
66 #include <aqbanking/types/transaction.h>
67 #include <aqbanking/types/transactionlimits.h>
68 #include <aqbanking/gui/abgui.h>
69 #include <aqbanking/version.h>
70 #include <gwenhywfar/logger.h>
71 #include <gwenhywfar/debug.h>
72 #include <gwenhywfar/version.h>
73 #include <gwenhywfar/gwenhywfar.h>
74 
75 // ----------------------------------------------------------------------------
76 // Project Includes
77 
78 #include "mymoney/onlinejob.h"
79 
80 #include "kbaccountsettings.h"
81 #include "kbmapaccount.h"
82 #include "mymoneyfile.h"
83 #include "onlinejobadministration.h"
84 #include "kmymoneyview.h"
85 #include "kbpickstartdate.h"
86 #include "mymoneyinstitution.h"
87 #include "mymoneytransactionfilter.h"
88 #include "mymoneyexception.h"
89 #include "mymoneysecurity.h"
90 
91 #include "gwenkdegui.h"
92 #include "gwenhywfarqtoperators.h"
93 #include "aqbankingkmmoperators.h"
94 #include "mymoneystatement.h"
95 #include "statementinterface.h"
96 #include "viewinterface.h"
97 
98 #ifdef KMM_DEBUG
99 #include "chiptandialog.h"
100 #include "phototandialog.h"
101 
102 #include "phototan-demo.cpp"
103 #endif
104 
105 class KBanking::Private
106 {
107 public:
Private()108   Private() :
109      passwordCacheTimer(nullptr),
110      jobList(),
111      fileId()
112   {
113     QString gwenProxy = QString::fromLocal8Bit(qgetenv("GWEN_PROXY"));
114     if (gwenProxy.isEmpty()) {
115       std::unique_ptr<KConfig> cfg = std::unique_ptr<KConfig>(new KConfig("kioslaverc"));
116       QRegExp exp("(\\w+://)?([^/]{2}.+:\\d+)");
117       QString proxy;
118 
119       KConfigGroup grp = cfg->group("Proxy Settings");
120       int type = grp.readEntry("ProxyType", 0);
121       switch (type) {
122         case 0: // no proxy
123           break;
124 
125         case 1: // manual specified
126           proxy = grp.readEntry("httpsProxy");
127           qDebug("KDE https proxy setting is '%s'", qPrintable(proxy));
128           if (exp.exactMatch(proxy)) {
129             proxy = exp.cap(2);
130             qDebug("Setting GWEN_PROXY to '%s'", qPrintable(proxy));
131             if (!qputenv("GWEN_PROXY", qPrintable(proxy))) {
132               qDebug("Unable to setup GWEN_PROXY");
133             }
134           }
135           break;
136 
137         default: // other currently not supported
138           qDebug("KDE proxy setting of type %d not supported", type);
139           break;
140       }
141     }
142   }
143 
libVersion(void (* version)(int *,int *,int *,int *))144   QString libVersion(void (*version)(int*, int*, int*, int*))
145   {
146     int major, minor, patch, build;
147     version(&major, &minor, &patch, &build);
148     return QString("%1.%2.%3.%4").arg(major).arg(minor).arg(patch).arg(build);
149   }
150   /**
151    * KMyMoney asks for accounts over and over again which causes a lot of "Job not supported with this account" error messages.
152    * This function filters messages with that string.
153    */
gwenLogHook(GWEN_GUI * gui,const char * domain,GWEN_LOGGER_LEVEL level,const char * message)154   static int gwenLogHook(GWEN_GUI* gui, const char* domain, GWEN_LOGGER_LEVEL level, const char* message) {
155     Q_UNUSED(gui);
156     Q_UNUSED(domain);
157     Q_UNUSED(level);
158 
159     const char* messageToFilter = "Job not supported with this account";
160     if (strstr(message, messageToFilter) != 0)
161       return 1;
162     return 0;
163   }
164 
165   QTimer *passwordCacheTimer;
166   QMap<QString, QStringList>  jobList;
167   QString                     fileId;
168   QSet<QAction *>             actions;
169 };
170 
171 
KBanking(QObject * parent,const QVariantList & args)172 KBanking::KBanking(QObject *parent, const QVariantList &args) :
173   OnlinePluginExtended(parent, "kbanking")
174   , d(new Private)
175   , m_configAction(nullptr)
176   , m_importAction(nullptr)
177   , m_kbanking(nullptr)
178   , m_accountSettings(nullptr)
179   , m_statementCount(0)
180 {
181   Q_UNUSED(args)
182   QString compileVersionSet = QLatin1String(GWENHYWFAR_VERSION_FULL_STRING "/" AQBANKING_VERSION_FULL_STRING);
183   QString runtimeVersionSet = QString("%1/%2").arg(d->libVersion(&GWEN_Version), d->libVersion(&AB_Banking_GetVersion));
184   qDebug() << QString("Plugins: kbanking loaded, build with (%1), run with (%2)").arg(compileVersionSet, runtimeVersionSet);
185 }
186 
~KBanking()187 KBanking::~KBanking()
188 {
189   delete d;
190   qDebug("Plugins: kbanking unloaded");
191 }
192 
plug()193 void KBanking::plug()
194 {
195   const auto componentName = QLatin1String("kbanking");
196   const auto rcFileName = QLatin1String("kbanking.rc");
197   m_kbanking = new KBankingExt(this, "KMyMoney");
198 
199   d->passwordCacheTimer = new QTimer(this);
200   d->passwordCacheTimer->setSingleShot(true);
201   d->passwordCacheTimer->setInterval(60000);
202   connect(d->passwordCacheTimer, &QTimer::timeout, this, &KBanking::slotClearPasswordCache);
203 
204   if (m_kbanking) {
205     //! @todo when is gwenKdeGui deleted?
206     gwenKdeGui *gui = new gwenKdeGui();
207     GWEN_Gui_SetGui(gui->getCInterface());
208     GWEN_Logger_SetLevel(0, GWEN_LoggerLevel_Warning);
209 
210     if (m_kbanking->init() == 0) {
211       // Tell the host application to load my GUI component
212       setComponentName(componentName, "KBanking");
213 
214 #ifdef IS_APPIMAGE
215       const QString rcFilePath = QString("%1/../share/kxmlgui5/%2/%3").arg(QCoreApplication::applicationDirPath(), componentName, rcFileName);
216       setXMLFile(rcFilePath);
217 
218       const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName;
219       setLocalXMLFile(localRcFilePath);
220 #else
221       setXMLFile(rcFileName);
222 #endif
223 
224       // get certificate handling and dialog settings management
225       AB_Gui_Extend(gui->getCInterface(), m_kbanking->getCInterface());
226 
227       // create actions
228       createActions();
229 
230       // load protocol conversion list
231       loadProtocolConversion();
232       GWEN_Logger_SetLevel(AQBANKING_LOGDOMAIN, GWEN_LoggerLevel_Warning);
233       GWEN_Gui_SetLogHookFn(GWEN_Gui_GetGui(), &KBanking::Private::gwenLogHook);
234 
235     } else {
236       qWarning("Could not initialize KBanking online banking interface");
237       delete m_kbanking;
238       m_kbanking = 0;
239     }
240   }
241 }
242 
unplug()243 void KBanking::unplug()
244 {
245   d->passwordCacheTimer->deleteLater();
246   if (m_kbanking) {
247     m_kbanking->fini();
248     delete m_kbanking;
249   }
250   // remove and delete the actions for this plugin
251   for (const auto& action : d->actions) {
252     actionCollection()->removeAction(action);
253   }
254   qDebug("Plugins: kbanking unplugged");
255 }
256 
257 
loadProtocolConversion()258 void KBanking::loadProtocolConversion()
259 {
260   if (m_kbanking) {
261     m_protocolConversionMap = {
262       {"aqhbci", "HBCI"},
263       {"aqofxconnect", "OFX"},
264       {"aqyellownet", "YellowNet"},
265       {"aqgeldkarte", "Geldkarte"},
266       {"aqdtaus", "DTAUS"}
267     };
268   }
269 }
270 
271 
protocols(QStringList & protocolList) const272 void KBanking::protocols(QStringList& protocolList) const
273 {
274   if (m_kbanking) {
275     std::list<std::string> list = m_kbanking->getActiveProviders();
276     std::list<std::string>::iterator it;
277     for (it = list.begin(); it != list.end(); ++it) {
278       // skip the dummy
279       if (*it == "aqnone")
280         continue;
281       QMap<QString, QString>::const_iterator it_m;
282       it_m = m_protocolConversionMap.find((*it).c_str());
283       if (it_m != m_protocolConversionMap.end())
284         protocolList << (*it_m);
285       else
286         protocolList << (*it).c_str();
287     }
288   }
289 }
290 
291 
accountConfigTab(const MyMoneyAccount & acc,QString & name)292 QWidget* KBanking::accountConfigTab(const MyMoneyAccount& acc, QString& name)
293 {
294   const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings();
295   name = i18n("Online settings");
296   if (m_kbanking) {
297     m_accountSettings = new KBAccountSettings(acc, 0);
298     m_accountSettings->loadUi(kvp);
299     return m_accountSettings;
300   }
301   QLabel* label = new QLabel(i18n("KBanking module not correctly initialized"), 0);
302   label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
303   return label;
304 }
305 
306 
onlineBankingSettings(const MyMoneyKeyValueContainer & current)307 MyMoneyKeyValueContainer KBanking::onlineBankingSettings(const MyMoneyKeyValueContainer& current)
308 {
309   MyMoneyKeyValueContainer kvp(current);
310   kvp["provider"] = objectName().toLower();
311   if (m_accountSettings) {
312     m_accountSettings->loadKvp(kvp);
313   }
314   return kvp;
315 }
316 
317 
createActions()318 void KBanking::createActions()
319 {
320   QAction *settings_aqbanking = actionCollection()->addAction("settings_aqbanking");
321   settings_aqbanking->setText(i18n("Configure Aq&Banking..."));
322   connect(settings_aqbanking, &QAction::triggered, this, &KBanking::slotSettings);
323   d->actions.insert(settings_aqbanking);
324 
325   QAction *file_import_aqbanking = actionCollection()->addAction("file_import_aqbanking");
326   file_import_aqbanking->setText(i18n("AqBanking importer..."));
327   connect(file_import_aqbanking, &QAction::triggered, this, &KBanking::slotImport);
328   d->actions.insert(file_import_aqbanking);
329 
330   Q_CHECK_PTR(viewInterface());
331   connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action("file_import_aqbanking"), &QAction::setEnabled);
332 
333 #ifdef KMM_DEBUG
334   QAction *openChipTanDialog = actionCollection()->addAction("open_chiptan_dialog");
335   openChipTanDialog->setText("Open ChipTan Dialog");
336   connect(openChipTanDialog, &QAction::triggered, [&](){
337     auto dlg = new chipTanDialog();
338     dlg->setHhdCode("0F04871100030333555414312C32331D");
339     dlg->setInfoText("<html><h1>Test Graphic for debugging</h1><p>The encoded data is</p><p>Account Number: <b>335554</b><br/>Amount: <b>1,23</b></p></html>");
340     connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater);
341     connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater);
342     dlg->show();
343   });
344   d->actions.insert(openChipTanDialog);
345 
346   QAction *openPhotoTanDialog = actionCollection()->addAction("open_phototan_dialog");
347   openPhotoTanDialog->setText("Open PhotoTan Dialog");
348   connect(openPhotoTanDialog, &QAction::triggered, [&](){
349     auto dlg = new photoTanDialog();
350     QImage img;
351     img.loadFromData(photoTan, sizeof(photoTan), "PNG");
352     img = img.scaled(300, 300, Qt::KeepAspectRatio);
353     dlg->setPicture(QPixmap::fromImage(img));
354     dlg->setInfoText("<html><h1>Test Graphic for debugging</h1><p>The encoded data is</p><p>unknown</p></html>");
355     connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater);
356     connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater);
357     dlg->show();
358   });
359   d->actions.insert(openPhotoTanDialog);
360 #endif
361 }
362 
slotSettings()363 void KBanking::slotSettings()
364 {
365   if (m_kbanking) {
366     GWEN_DIALOG* dlg = AB_Banking_CreateSetupDialog(m_kbanking->getCInterface());
367     if (dlg == NULL) {
368       DBG_ERROR(0, "Could not create setup dialog.");
369       return;
370     }
371 
372     if (GWEN_Gui_ExecDialog(dlg, 0) == 0) {
373       DBG_ERROR(0, "Aborted by user");
374       GWEN_Dialog_free(dlg);
375       return;
376     }
377     GWEN_Dialog_free(dlg);
378   }
379 }
380 
381 
mapAccount(const MyMoneyAccount & acc,MyMoneyKeyValueContainer & settings)382 bool KBanking::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings)
383 {
384   bool rc = false;
385   if (m_kbanking && !acc.id().isEmpty()) {
386     m_kbanking->askMapAccount(acc);
387 
388     // at this point, the account should be mapped
389     // so we search it and setup the account reference in the KMyMoney object
390     AB_ACCOUNT_SPEC* ab_acc;
391     ab_acc = aqbAccount(acc);
392     if (ab_acc) {
393       MyMoneyAccount a(acc);
394       setupAccountReference(a, ab_acc);
395       settings = a.onlineBankingSettings();
396       rc = true;
397     }
398   }
399   return rc;
400 }
401 
402 
aqbAccount(const MyMoneyAccount & acc) const403 AB_ACCOUNT_SPEC* KBanking::aqbAccount(const MyMoneyAccount& acc) const
404 {
405   if (m_kbanking == 0) {
406     return 0;
407   }
408 
409   // certainly looking for an expense or income account does not make sense at this point
410   // so we better get out right away
411   if (acc.isIncomeExpense()) {
412     return 0;
413   }
414 
415   AB_ACCOUNT_SPEC *ab_acc = AB_Banking_GetAccountSpecByAlias(m_kbanking->getCInterface(), m_kbanking->mappingId(acc).toUtf8().data());
416   // if the account is not found, we temporarily scan for the 'old' mapping (the one w/o the file id)
417   // in case we find it, we setup the new mapping in addition on the fly.
418   if (!ab_acc && acc.isAssetLiability()) {
419     ab_acc = AB_Banking_GetAccountSpecByAlias(m_kbanking->getCInterface(), acc.id().toUtf8().data());
420     if (ab_acc) {
421       qDebug("Found old mapping for '%s' but not new. Setup new mapping", qPrintable(acc.name()));
422       m_kbanking->setAccountAlias(ab_acc, m_kbanking->mappingId(acc).toUtf8().constData());
423       // TODO at some point in time, we should remove the old mapping
424     }
425   }
426   return ab_acc;
427 }
428 
429 
aqbAccount(const QString & accountId) const430 AB_ACCOUNT_SPEC* KBanking::aqbAccount(const QString& accountId) const
431 {
432   MyMoneyAccount account = MyMoneyFile::instance()->account(accountId);
433   return aqbAccount(account);
434 }
435 
436 
stripLeadingZeroes(const QString & s) const437 QString KBanking::stripLeadingZeroes(const QString& s) const
438 {
439   QString rc(s);
440   QRegExp exp("^(0*)([^0].*)");
441   if (exp.exactMatch(s)) {
442     rc = exp.cap(2);
443   }
444   return rc;
445 }
446 
setupAccountReference(const MyMoneyAccount & acc,AB_ACCOUNT_SPEC * ab_acc)447 void KBanking::setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT_SPEC* ab_acc)
448 {
449   MyMoneyKeyValueContainer kvp;
450 
451   if (ab_acc) {
452     QString accountNumber = stripLeadingZeroes(AB_AccountSpec_GetAccountNumber(ab_acc));
453     QString routingNumber = stripLeadingZeroes(AB_AccountSpec_GetBankCode(ab_acc));
454 
455     QString val = QString("%1-%2-%3").arg(routingNumber, accountNumber).arg(AB_AccountSpec_GetType(ab_acc));
456 
457     if (val != acc.onlineBankingSettings().value("kbanking-acc-ref")) {
458       kvp.clear();
459 
460       // make sure to keep our own previous settings
461       const QMap<QString, QString>& vals = acc.onlineBankingSettings().pairs();
462       QMap<QString, QString>::const_iterator it_p;
463       for (it_p = vals.begin(); it_p != vals.end(); ++it_p) {
464         if (QString(it_p.key()).startsWith("kbanking-")) {
465           kvp.setValue(it_p.key(), *it_p);
466         }
467       }
468 
469       kvp.setValue("kbanking-acc-ref", val);
470       kvp.setValue("provider", objectName().toLower());
471       setAccountOnlineParameters(acc, kvp);
472     }
473   } else {
474     // clear the connection
475     setAccountOnlineParameters(acc, kvp);
476   }
477 }
478 
479 
accountIsMapped(const MyMoneyAccount & acc)480 bool KBanking::accountIsMapped(const MyMoneyAccount& acc)
481 {
482   return aqbAccount(acc) != 0;
483 }
484 
485 
updateAccount(const MyMoneyAccount & acc)486 bool KBanking::updateAccount(const MyMoneyAccount& acc)
487 {
488   return updateAccount(acc, false);
489 }
490 
491 
updateAccount(const MyMoneyAccount & acc,bool moreAccounts)492 bool KBanking::updateAccount(const MyMoneyAccount& acc, bool moreAccounts)
493 {
494   if (!m_kbanking)
495     return false;
496 
497   bool rc = false;
498 
499   if (!acc.id().isEmpty()) {
500     AB_TRANSACTION *job = 0;
501     int rv;
502 
503     /* get AqBanking account */
504     AB_ACCOUNT_SPEC *ba = aqbAccount(acc);
505     // Update the connection between the KMyMoney account and the AqBanking equivalent.
506     // If the account is not found anymore ba == 0 and the connection is removed.
507     setupAccountReference(acc, ba);
508 
509     if (!ba) {
510       KMessageBox::error(0,
511                          i18n("<qt>"
512                               "The given application account <b>%1</b> "
513                               "has not been mapped to an online "
514                               "account."
515                               "</qt>",
516                               acc.name()),
517                          i18n("Account Not Mapped"));
518     } else {
519       bool enqueJob = true;
520       if (acc.onlineBankingSettings().value("kbanking-txn-download") != "no") {
521         /* create getTransactions job */
522         if (AB_AccountSpec_GetTransactionLimitsForCommand(ba, AB_Transaction_CommandGetTransactions)) {
523           /* there are transaction limits for this job, so it is allowed */
524           job = AB_Transaction_new();
525           AB_Transaction_SetUniqueAccountId(job, AB_AccountSpec_GetUniqueId(ba));
526           AB_Transaction_SetCommand(job, AB_Transaction_CommandGetTransactions);
527         }
528 
529         if (job) {
530           int days = 0 /* TODO in AqBanking AB_JobGetTransactions_GetMaxStoreDays(job)*/;
531           QDate qd;
532           if (days > 0) {
533             GWEN_DATE *dt;
534 
535             dt=GWEN_Date_CurrentDate();
536             GWEN_Date_SubDays(dt, days);
537             qd = QDate(GWEN_Date_GetYear(dt), GWEN_Date_GetMonth(dt), GWEN_Date_GetDay(dt));
538             GWEN_Date_free(dt);
539           }
540 
541           // get last statement request date from application account object
542           // and start from a few days before if the date is valid
543           QDate lastUpdate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate);
544           if (lastUpdate.isValid())
545             lastUpdate = lastUpdate.addDays(-3);
546 
547           int dateOption = acc.onlineBankingSettings().value("kbanking-statementDate").toInt();
548           switch (dateOption) {
549             case 0: // Ask user
550               break;
551             case 1: // No date
552               qd = QDate();
553               break;
554             case 2: // Last download
555               qd = lastUpdate;
556               break;
557             case 3: // First possible
558               // qd is already setup
559               break;
560           }
561 
562           // the pick start date option dialog is needed in
563           // case the dateOption is 0 or the date option is > 1
564           // and the qd is invalid
565           if (dateOption == 0 || (dateOption > 1 && !qd.isValid())) {
566             QPointer<KBPickStartDate> psd = new KBPickStartDate(m_kbanking, qd, lastUpdate, acc.name(),
567                 lastUpdate.isValid() ? 2 : 3, 0, true);
568             if (psd->exec() == QDialog::Accepted) {
569               qd = psd->date();
570             } else {
571               enqueJob = false;
572             }
573             delete psd;
574           }
575 
576           if (enqueJob) {
577             if (qd.isValid()) {
578               GWEN_DATE *dt;
579 
580               dt=GWEN_Date_fromGregorian(qd.year(), qd.month(), qd.day());
581               AB_Transaction_SetFirstDate(job, dt);
582               GWEN_Date_free(dt);
583             }
584 
585             rv = m_kbanking->enqueueJob(job);
586             if (rv) {
587               DBG_ERROR(0, "Error %d", rv);
588               KMessageBox::error(0,
589                                 i18n("<qt>"
590                                       "Could not enqueue the job.\n"
591                                       "</qt>"),
592                                 i18n("Error"));
593             }
594           }
595           AB_Transaction_free(job);
596         }
597       }
598 
599       if (enqueJob) {
600         /* create getBalance job */
601         if (AB_AccountSpec_GetTransactionLimitsForCommand(ba, AB_Transaction_CommandGetBalance)) {
602           /* there are transaction limits for this job, so it is allowed */
603           job = AB_Transaction_new();
604           AB_Transaction_SetUniqueAccountId(job, AB_AccountSpec_GetUniqueId(ba));
605           AB_Transaction_SetCommand(job, AB_Transaction_CommandGetBalance);
606           rv = m_kbanking->enqueueJob(job);
607           AB_Transaction_free(job);
608           if (rv) {
609             DBG_ERROR(0, "Error %d", rv);
610             KMessageBox::error(0,
611                                i18n("<qt>"
612                                     "Could not enqueue the job.\n"
613                                     "</qt>"),
614                                i18n("Error"));
615           } else {
616             rc = true;
617             emit queueChanged();
618           }
619         }
620       }
621     }
622   }
623 
624   // make sure we have at least one job in the queue before sending it
625   if (!moreAccounts && m_kbanking->getEnqueuedJobs().size() > 0)
626     executeQueue();
627 
628   return rc;
629 }
630 
631 
executeQueue()632 void KBanking::executeQueue()
633 {
634   if (m_kbanking && m_kbanking->getEnqueuedJobs().size() > 0) {
635     AB_IMEXPORTER_CONTEXT *ctx;
636     ctx = AB_ImExporterContext_new();
637     int rv = m_kbanking->executeQueue(ctx);
638     if (!rv) {
639       m_kbanking->importContext(ctx, 0);
640     } else {
641       DBG_ERROR(0, "Error: %d", rv);
642     }
643     AB_ImExporterContext_free(ctx);
644   }
645 }
646 
647 
648 /** @todo improve error handling, e.g. by adding a .isValid to nationalTransfer
649  * @todo use new onlineJob system
650  */
sendOnlineJob(QList<onlineJob> & jobs)651 void KBanking::sendOnlineJob(QList<onlineJob>& jobs)
652 {
653   Q_CHECK_PTR(m_kbanking);
654 
655   m_onlineJobQueue.clear();
656   QList<onlineJob> unhandledJobs;
657 
658   if (!jobs.isEmpty()) {
659     foreach (onlineJob job, jobs) {
660       if (sepaOnlineTransfer::name() == job.task()->taskName()) {
661         onlineJobTyped<sepaOnlineTransfer> typedJob(job);
662         enqueTransaction(typedJob);
663         job = typedJob;
664       } else {
665         job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "KBanking", "Cannot handle this request"));
666         unhandledJobs.append(job);
667       }
668       m_onlineJobQueue.insert(m_kbanking->mappingId(job), job);
669     }
670 
671     executeQueue();
672   }
673   jobs = m_onlineJobQueue.values() + unhandledJobs;
674   m_onlineJobQueue.clear();
675 }
676 
677 
availableJobs(QString accountId)678 QStringList KBanking::availableJobs(QString accountId)
679 {
680   try {
681     MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId);
682     QString id = MyMoneyFile::instance()->value("kmm-id");
683     if(id != d->fileId) {
684       d->jobList.clear();
685       d->fileId = id;
686     }
687   } catch (const MyMoneyException &) {
688     // Exception usually means account was not found
689     return QStringList();
690   }
691 
692   if(d->jobList.contains(accountId)) {
693     return d->jobList[accountId];
694   }
695 
696   QStringList list;
697   AB_ACCOUNT_SPEC* abAccount = aqbAccount(accountId);
698 
699   if (!abAccount) {
700     return list;
701   }
702 
703   // Check availableJobs
704 
705   // sepa transfer
706   if (AB_AccountSpec_GetTransactionLimitsForCommand(abAccount, AB_Transaction_CommandSepaTransfer)) {
707     list.append(sepaOnlineTransfer::name());
708   }
709 
710   d->jobList[accountId] = list;
711   return list;
712 }
713 
714 
715 /** @brief experimenting with QScopedPointer and aqBanking pointers */
716 class QScopedPointerAbJobDeleter
717 {
718 public:
cleanup(AB_TRANSACTION * job)719   static void cleanup(AB_TRANSACTION* job) {
720     AB_Transaction_free(job);
721   }
722 };
723 
724 
725 /** @brief experimenting with QScopedPointer and aqBanking pointers */
726 class QScopedPointerAbAccountDeleter
727 {
728 public:
cleanup(AB_ACCOUNT_SPEC * account)729   static void cleanup(AB_ACCOUNT_SPEC* account) {
730     AB_AccountSpec_free(account);
731   }
732 };
733 
734 
settings(QString accountId,QString taskName)735 IonlineTaskSettings::ptr KBanking::settings(QString accountId, QString taskName)
736 {
737   AB_ACCOUNT_SPEC* abAcc = aqbAccount(accountId);
738   if (abAcc == 0)
739     return IonlineTaskSettings::ptr();
740 
741   if (sepaOnlineTransfer::name() == taskName) {
742     // Get limits for sepaonlinetransfer
743     const AB_TRANSACTION_LIMITS *limits=AB_AccountSpec_GetTransactionLimitsForCommand(abAcc, AB_Transaction_CommandSepaTransfer);
744     if (limits==NULL)
745       return IonlineTaskSettings::ptr();
746     return AB_TransactionLimits_toSepaOnlineTaskSettings(limits).dynamicCast<IonlineTaskSettings>();
747   }
748   return IonlineTaskSettings::ptr();
749 }
750 
751 
enqueTransaction(onlineJobTyped<sepaOnlineTransfer> & job)752 bool KBanking::enqueTransaction(onlineJobTyped<sepaOnlineTransfer>& job)
753 {
754   /* get AqBanking account */
755   const QString accId = job.constTask()->responsibleAccount();
756 
757   AB_ACCOUNT_SPEC *abAccount = aqbAccount(accId);
758   if (!abAccount) {
759     job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Warning, "KBanking", i18n("<qt>"
760                                                                                     "The given application account <b>%1</b> "
761                                                                                     "has not been mapped to an online "
762                                                                                     "account."
763                                                                                     "</qt>",
764                                        MyMoneyFile::instance()->account(accId).name())));
765     return false;
766   }
767   //setupAccountReference(acc, ba); // needed?
768 
769   if (AB_AccountSpec_GetTransactionLimitsForCommand(abAccount, AB_Transaction_CommandSepaTransfer)==NULL) {
770     qDebug("AB_ERROR_OFFSET is %i", AB_ERROR_OFFSET);
771     job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "AqBanking",
772                                        QString("Sepa credit transfers for account \"%1\" are not available.").arg(MyMoneyFile::instance()->account(accId).name())
773                                       )
774                      );
775     return false;
776   }
777 
778 
779   AB_TRANSACTION *abJob = AB_Transaction_new();
780 
781   /* command */
782   AB_Transaction_SetCommand(abJob, AB_Transaction_CommandSepaTransfer);
783 
784   // Origin Account
785   AB_Transaction_SetUniqueAccountId(abJob, AB_AccountSpec_GetUniqueId(abAccount));
786 
787   // Recipient
788   payeeIdentifiers::ibanBic beneficiaryAcc = job.constTask()->beneficiaryTyped();
789   AB_Transaction_SetRemoteName(abJob, beneficiaryAcc.ownerName().toUtf8().constData());
790   AB_Transaction_SetRemoteIban(abJob, beneficiaryAcc.electronicIban().toUtf8().constData());
791   AB_Transaction_SetRemoteBic(abJob, beneficiaryAcc.fullStoredBic().toUtf8().constData());
792 
793   // Origin Account
794   AB_Transaction_SetLocalAccount(abJob, abAccount);
795 
796   // Purpose
797   AB_Transaction_SetPurpose(abJob, job.constTask()->purpose().toUtf8().constData());
798 
799   // Reference
800   // AqBanking duplicates the string. This should be safe.
801   AB_Transaction_SetEndToEndReference(abJob, job.constTask()->endToEndReference().toUtf8().constData());
802 
803   // Other Fields
804   AB_Transaction_SetTextKey(abJob, job.constTask()->textKey());
805   AB_Transaction_SetValue(abJob, AB_Value_fromMyMoneyMoney(job.constTask()->value()));
806 
807   /** @todo LOW remove Debug info */
808   AB_Transaction_SetStringIdForApplication(abJob, m_kbanking->mappingId(job).toUtf8().constData());
809 
810   qDebug() << "Enqueue: " << m_kbanking->enqueueJob(abJob);
811 
812   AB_Transaction_free(abJob);
813 
814   //delete localAcc;
815   return true;
816 }
817 
818 
startPasswordTimer()819 void KBanking::startPasswordTimer()
820 {
821   if (d->passwordCacheTimer->isActive())
822     d->passwordCacheTimer->stop();
823   d->passwordCacheTimer->start();
824 }
825 
826 
slotClearPasswordCache()827 void KBanking::slotClearPasswordCache()
828 {
829   m_kbanking->clearPasswordCache();
830 }
831 
832 
slotImport()833 void KBanking::slotImport()
834 {
835   m_statementCount = 0;
836   statementInterface()->resetMessages();
837 
838   if (!m_kbanking->interactiveImport())
839     qWarning("Error on import dialog");
840   else
841     statementInterface()->showMessages(m_statementCount);
842 }
843 
844 
importStatement(const MyMoneyStatement & s)845 bool KBanking::importStatement(const MyMoneyStatement& s)
846 {
847   m_statementCount++;
848   return !statementInterface()->import(s).isEmpty();
849 }
850 
851 
account(const QString & key,const QString & value) const852 MyMoneyAccount KBanking::account(const QString& key, const QString& value) const
853 {
854   return statementInterface()->account(key, value);
855 }
856 
857 
setAccountOnlineParameters(const MyMoneyAccount & acc,const MyMoneyKeyValueContainer & kvps) const858 void KBanking::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const
859 {
860   return statementInterface()->setAccountOnlineParameters(acc, kvps);
861 }
862 
863 
KBankingExt(KBanking * parent,const char * appname,const char * fname)864 KBankingExt::KBankingExt(KBanking* parent, const char* appname, const char* fname)
865     : AB_Banking(appname, fname)
866     , m_parent(parent)
867     , _jobQueue(0)
868 {
869   m_sepaKeywords = {QString::fromUtf8("SEPA-BASISLASTSCHRIFT"), QString::fromUtf8("SEPA-ÜBERWEISUNG")};
870   QRegularExpression exp("(\\d+\\.\\d+\\.\\d+).*");
871   QRegularExpressionMatch match = exp.match(KAboutData::applicationData().version());
872 
873   QByteArray regkey;
874   const char *p = "\x08\x0f\x41\x0f\x58\x5b\x56\x4a\x09\x7b\x40\x0e\x5f\x2a\x56\x3f\x0e\x7f\x3f\x7d\x5b\x56\x56\x4b\x0a\x4d";
875   const char* q = appname;
876   while (*p) {
877     regkey += (*q++ ^ *p++) & 0xff;
878     if (!*q)
879       q = appname;
880   }
881   registerFinTs(regkey.data(), match.captured(1).remove(QLatin1Char('.')).left(5).toLatin1());
882 }
883 
884 
init()885 int KBankingExt::init()
886 {
887   int rv = AB_Banking::init();
888   if (rv < 0)
889     return rv;
890 
891   _jobQueue = AB_Transaction_List2_new();
892   return 0;
893 }
894 
895 
fini()896 int KBankingExt::fini()
897 {
898   if (_jobQueue) {
899     AB_Transaction_List2_freeAll(_jobQueue);
900     _jobQueue = 0;
901   }
902 
903   return AB_Banking::fini();
904 }
905 
906 
executeQueue(AB_IMEXPORTER_CONTEXT * ctx)907 int KBankingExt::executeQueue(AB_IMEXPORTER_CONTEXT *ctx)
908 {
909   m_parent->startPasswordTimer();
910 
911   int rv = AB_Banking::executeJobs(_jobQueue, ctx);
912   if (rv != 0) {
913     qDebug() << "Sending queue by aqbanking got error no " << rv;
914   }
915 
916   /** check result of each job */
917   AB_TRANSACTION_LIST2_ITERATOR* jobIter = AB_Transaction_List2_First(_jobQueue);
918   if (jobIter) {
919     AB_TRANSACTION* abJob = AB_Transaction_List2Iterator_Data(jobIter);
920 
921     while (abJob) {
922       const char *stringIdForApp=AB_Transaction_GetStringIdForApplication(abJob);
923 
924       if (!(stringIdForApp && *stringIdForApp)) {
925         qWarning("Executed AB_Job without KMyMoney id");
926         abJob = AB_Transaction_List2Iterator_Next(jobIter);
927         continue;
928       }
929       QString jobIdent = QString::fromUtf8(stringIdForApp);
930 
931       onlineJob job = m_parent->m_onlineJobQueue.value(jobIdent);
932       if (job.isNull()) {
933         // It should not be possible that this will happen (only if AqBanking fails heavily).
934         //! @todo correct exception text
935         qWarning("Executed a job which was not in queue. Please inform the KMyMoney developers.");
936         abJob = AB_Transaction_List2Iterator_Next(jobIter);
937         continue;
938       }
939 
940       AB_TRANSACTION_STATUS abStatus = AB_Transaction_GetStatus(abJob);
941 
942       if (abStatus == AB_Transaction_StatusSent
943           || abStatus == AB_Transaction_StatusPending
944           || abStatus == AB_Transaction_StatusAccepted
945           || abStatus == AB_Transaction_StatusRejected
946           || abStatus == AB_Transaction_StatusError
947           || abStatus == AB_Transaction_StatusUnknown)
948         job.setJobSend();
949 
950       if (abStatus == AB_Transaction_StatusAccepted)
951         job.setBankAnswer(eMyMoney::OnlineJob::sendingState::acceptedByBank);
952       else if (abStatus == AB_Transaction_StatusError
953                || abStatus == AB_Transaction_StatusRejected
954                || abStatus == AB_Transaction_StatusUnknown)
955         job.setBankAnswer(eMyMoney::OnlineJob::sendingState::sendingError);
956 
957       job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KBanking", "Job was processed"));
958       m_parent->m_onlineJobQueue.insert(jobIdent, job);
959       abJob = AB_Transaction_List2Iterator_Next(jobIter);
960     }
961     AB_Transaction_List2Iterator_free(jobIter);
962   }
963 
964   AB_TRANSACTION_LIST2 *oldQ = _jobQueue;
965   _jobQueue = AB_Transaction_List2_new();
966   AB_Transaction_List2_freeAll(oldQ);
967 
968   emit m_parent->queueChanged();
969   m_parent->startPasswordTimer();
970 
971   return rv;
972 }
973 
974 
clearPasswordCache()975 void KBankingExt::clearPasswordCache()
976 {
977   /* clear password DB */
978   GWEN_Gui_SetPasswordStatus(NULL, NULL, GWEN_Gui_PasswordStatus_Remove, 0);
979 }
980 
981 
getEnqueuedJobs()982 std::list<AB_TRANSACTION*> KBankingExt::getEnqueuedJobs()
983 {
984   AB_TRANSACTION_LIST2 *ll;
985   std::list<AB_TRANSACTION*> rl;
986 
987   ll = _jobQueue;
988   if (ll && AB_Transaction_List2_GetSize(ll)) {
989     AB_TRANSACTION *j;
990     AB_TRANSACTION_LIST2_ITERATOR *it;
991 
992     it = AB_Transaction_List2_First(ll);
993     assert(it);
994     j = AB_Transaction_List2Iterator_Data(it);
995     assert(j);
996     while (j) {
997       rl.push_back(j);
998       j = AB_Transaction_List2Iterator_Next(it);
999     }
1000     AB_Transaction_List2Iterator_free(it);
1001   }
1002   return rl;
1003 }
1004 
1005 
enqueueJob(AB_TRANSACTION * j)1006 int KBankingExt::enqueueJob(AB_TRANSACTION *j)
1007 {
1008   assert(_jobQueue);
1009   assert(j);
1010   AB_Transaction_Attach(j);
1011   AB_Transaction_List2_PushBack(_jobQueue, j);
1012   return 0;
1013 }
1014 
1015 
dequeueJob(AB_TRANSACTION * j)1016 int KBankingExt::dequeueJob(AB_TRANSACTION *j)
1017 {
1018   assert(_jobQueue);
1019   AB_Transaction_List2_Remove(_jobQueue, j);
1020   AB_Transaction_free(j);
1021   emit m_parent->queueChanged();
1022   return 0;
1023 }
1024 
1025 
transfer()1026 void KBankingExt::transfer()
1027 {
1028   //m_parent->transfer();
1029 }
1030 
1031 
askMapAccount(const MyMoneyAccount & acc)1032 bool KBankingExt::askMapAccount(const MyMoneyAccount& acc)
1033 {
1034   MyMoneyFile* file = MyMoneyFile::instance();
1035 
1036   QString bankId;
1037   QString accountId;
1038   // extract some information about the bank. if we have a sortcode
1039   // (BLZ) we display it, otherwise the name is enough.
1040   try {
1041     const MyMoneyInstitution &bank = file->institution(acc.institutionId());
1042     bankId = bank.name();
1043     if (!bank.sortcode().isEmpty())
1044       bankId = bank.sortcode();
1045   } catch (const MyMoneyException &e) {
1046     // no bank assigned, we just leave the field empty
1047   }
1048 
1049   // extract account information. if we have an account number
1050   // we show it, otherwise the name will be displayed
1051   accountId = acc.number();
1052   if (accountId.isEmpty())
1053     accountId = acc.name();
1054 
1055   // do the mapping. the return value of this method is either
1056   // true, when the user mapped the account or false, if he
1057   // decided to quit the dialog. So not really a great thing
1058   // to present some more information.
1059 
1060   KBMapAccount *w;
1061   w = new KBMapAccount(this,
1062                        bankId.toUtf8().constData(),
1063                        accountId.toUtf8().constData());
1064   if (w->exec() == QDialog::Accepted) {
1065     AB_ACCOUNT_SPEC *a;
1066 
1067     a = w->getAccount();
1068     assert(a);
1069     DBG_NOTICE(0,
1070                "Mapping application account \"%s\" to "
1071                "online account \"%s/%s\"",
1072                qPrintable(acc.name()),
1073                AB_AccountSpec_GetBankCode(a),
1074                AB_AccountSpec_GetAccountNumber(a));
1075 
1076     // TODO remove the following line once we don't need backward compatibility
1077     setAccountAlias(a, acc.id().toUtf8().constData());
1078     qDebug("Setup mapping to '%s'", acc.id().toUtf8().constData());
1079 
1080     setAccountAlias(a, mappingId(acc).toUtf8().constData());
1081     qDebug("Setup mapping to '%s'", mappingId(acc).toUtf8().constData());
1082 
1083     delete w;
1084     return true;
1085   }
1086 
1087   delete w;
1088   return false;
1089 }
1090 
1091 
mappingId(const MyMoneyObject & object) const1092 QString KBankingExt::mappingId(const MyMoneyObject& object) const
1093 {
1094   QString id = MyMoneyFile::instance()->storageId() + QLatin1Char('-') + object.id();
1095 
1096   // AqBanking does not handle the enclosing parens, so we remove it
1097   id.remove('{');
1098   id.remove('}');
1099   return id;
1100 }
1101 
1102 
interactiveImport()1103 bool KBankingExt::interactiveImport()
1104 {
1105   AB_IMEXPORTER_CONTEXT *ctx;
1106   GWEN_DIALOG *dlg;
1107   int rv;
1108 
1109   ctx = AB_ImExporterContext_new();
1110   dlg = AB_Banking_CreateImporterDialog(getCInterface(), ctx, NULL);
1111   if (dlg == NULL) {
1112     DBG_ERROR(0, "Could not create importer dialog.");
1113     AB_ImExporterContext_free(ctx);
1114     return false;
1115   }
1116 
1117   rv = GWEN_Gui_ExecDialog(dlg, 0);
1118   if (rv == 0) {
1119     DBG_ERROR(0, "Aborted by user");
1120     GWEN_Dialog_free(dlg);
1121     AB_ImExporterContext_free(ctx);
1122     return false;
1123   }
1124 
1125   if (!importContext(ctx, 0)) {
1126     DBG_ERROR(0, "Error on importContext");
1127     GWEN_Dialog_free(dlg);
1128     AB_ImExporterContext_free(ctx);
1129     return false;
1130   }
1131 
1132   GWEN_Dialog_free(dlg);
1133   AB_ImExporterContext_free(ctx);
1134   return true;
1135 }
1136 
1137 
1138 
_xaToStatement(MyMoneyStatement & ks,const MyMoneyAccount & acc,const AB_TRANSACTION * t)1139 void KBankingExt::_xaToStatement(MyMoneyStatement &ks,
1140                                  const MyMoneyAccount& acc,
1141                                  const AB_TRANSACTION *t)
1142 {
1143   QString s;
1144   QString memo;
1145   const char *p;
1146   const AB_VALUE *val;
1147   const GWEN_DATE *dt;
1148   const GWEN_DATE *startDate = 0;
1149   MyMoneyStatement::Transaction kt;
1150   unsigned long h;
1151 
1152   kt.m_fees = MyMoneyMoney();
1153 
1154   // bank's transaction id
1155   p = AB_Transaction_GetFiId(t);
1156   if (p)
1157     kt.m_strBankID = QString("ID ") + QString::fromUtf8(p);
1158 
1159   // payee
1160   p = AB_Transaction_GetRemoteName(t);
1161   if (p)
1162     kt.m_strPayee = QString::fromUtf8(p);
1163 
1164   // memo
1165 
1166   p = AB_Transaction_GetPurpose(t);
1167   if (p && *p) {
1168     QString tmpMemo;
1169 
1170     s=QString::fromUtf8(p).trimmed();
1171     tmpMemo=QString::fromUtf8(p).trimmed();
1172     tmpMemo.replace('\n', ' ');
1173 
1174     memo=tmpMemo;
1175   }
1176 
1177   // in case we have some SEPA fields filled with information
1178   // we add them to the memo field
1179   p = AB_Transaction_GetEndToEndReference(t);
1180   if (p) {
1181     s += QString(", EREF: %1").arg(p);
1182     if(memo.length())
1183       memo.append('\n');
1184     memo.append(QString("EREF: %1").arg(p));
1185   }
1186   p = AB_Transaction_GetCustomerReference(t);
1187   if (p) {
1188     s += QString(", CREF: %1").arg(p);
1189     if(memo.length())
1190       memo.append('\n');
1191     memo.append(QString("CREF: %1").arg(p));
1192   }
1193   p = AB_Transaction_GetMandateId(t);
1194   if (p) {
1195     s += QString(", MREF: %1").arg(p);
1196     if(memo.length())
1197       memo.append('\n');
1198     memo.append(QString("MREF: %1").arg(p));
1199   }
1200   p = AB_Transaction_GetCreditorSchemeId(t);
1201   if (p) {
1202     s += QString(", CRED: %1").arg(p);
1203     if(memo.length())
1204       memo.append('\n');
1205     memo.append(QString("CRED: %1").arg(p));
1206   }
1207   p = AB_Transaction_GetOriginatorId(t);
1208   if (p) {
1209     s += QString(", DEBT: %1").arg(p);
1210     if(memo.length())
1211       memo.append('\n');
1212     memo.append(QString("DEBT: %1").arg(p));
1213   }
1214 
1215   const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings();
1216   // check if we need the version with or without linebreaks
1217   if (kvp.value("kbanking-memo-removelinebreaks").compare(QLatin1String("no"))) {
1218     kt.m_strMemo = memo;
1219   } else {
1220     kt.m_strMemo = s;
1221   }
1222 
1223   // calculate the hash code and start with the payee info
1224   // and append the memo field
1225   h = MyMoneyTransaction::hash(kt.m_strPayee.trimmed());
1226   h = MyMoneyTransaction::hash(s, h);
1227 
1228   // see, if we need to extract the payee from the memo field
1229   QString rePayee = kvp.value("kbanking-payee-regexp");
1230   if (!rePayee.isEmpty() && kt.m_strPayee.isEmpty()) {
1231     QString reMemo = kvp.value("kbanking-memo-regexp");
1232     QStringList exceptions = kvp.value("kbanking-payee-exceptions").split(';', QString::SkipEmptyParts);
1233 
1234     bool needExtract = true;
1235     QStringList::const_iterator it_s;
1236     for (it_s = exceptions.constBegin(); needExtract && it_s != exceptions.constEnd(); ++it_s) {
1237       QRegExp exp(*it_s, Qt::CaseInsensitive);
1238       if (exp.indexIn(kt.m_strMemo) != -1) {
1239         needExtract = false;
1240       }
1241     }
1242     if (needExtract) {
1243       QRegExp expPayee(rePayee, Qt::CaseInsensitive);
1244       QRegExp expMemo(reMemo, Qt::CaseInsensitive);
1245       if (expPayee.indexIn(kt.m_strMemo) != -1) {
1246         kt.m_strPayee = expPayee.cap(1);
1247         if (expMemo.indexIn(kt.m_strMemo) != -1) {
1248           kt.m_strMemo = expMemo.cap(1);
1249         }
1250       }
1251     }
1252   }
1253 
1254   kt.m_strPayee = kt.m_strPayee.trimmed();
1255 
1256   // date
1257   dt = AB_Transaction_GetDate(t);
1258   if (!dt)
1259     dt = AB_Transaction_GetValutaDate(t);
1260   if (dt) {
1261     if (!startDate)
1262       startDate = dt;
1263     kt.m_datePosted = QDate(GWEN_Date_GetYear(dt), GWEN_Date_GetMonth(dt), GWEN_Date_GetDay(dt));
1264   } else {
1265     DBG_WARN(0, "No date for transaction");
1266   }
1267 
1268   // value
1269   val = AB_Transaction_GetValue(t);
1270   if (val) {
1271     if (ks.m_strCurrency.isEmpty()) {
1272       p = AB_Value_GetCurrency(val);
1273       if (p)
1274         ks.m_strCurrency = p;
1275     } else {
1276       p = AB_Value_GetCurrency(val);
1277       if (p)
1278         s = p;
1279       if (ks.m_strCurrency.toLower() != s.toLower()) {
1280         // TODO: handle currency difference
1281         DBG_ERROR(0, "Mixed currencies currently not allowed");
1282       }
1283     }
1284 
1285     kt.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val));
1286     // The initial implementation of this feature was based on
1287     // a denominator of 100. Since the denominator might be
1288     // different nowadays, we make sure to use 100 for the
1289     // duplicate detection
1290     QString tmpVal = kt.m_amount.formatMoney(100, false);
1291     tmpVal.remove(QRegExp("[,\\.]"));
1292     tmpVal += QLatin1String("/100");
1293     h = MyMoneyTransaction::hash(tmpVal, h);
1294   } else {
1295     DBG_WARN(0, "No value for transaction");
1296   }
1297 
1298   if (startDate) {
1299     QDate d(QDate(GWEN_Date_GetYear(startDate), GWEN_Date_GetMonth(startDate), GWEN_Date_GetDay(startDate)));
1300 
1301     if (!ks.m_dateBegin.isValid())
1302       ks.m_dateBegin = d;
1303     else if (d < ks.m_dateBegin)
1304       ks.m_dateBegin = d;
1305 
1306     if (!ks.m_dateEnd.isValid())
1307       ks.m_dateEnd = d;
1308     else if (d > ks.m_dateEnd)
1309       ks.m_dateEnd = d;
1310   } else {
1311     DBG_WARN(0, "No date in current transaction");
1312   }
1313 
1314   // add information about remote account to memo in case we have something
1315   const char *remoteAcc = AB_Transaction_GetRemoteAccountNumber(t);
1316   const char *remoteBankCode = AB_Transaction_GetRemoteBankCode(t);
1317   if (remoteAcc && remoteBankCode) {
1318     kt.m_strMemo += QString("\n%1/%2").arg(remoteBankCode, remoteAcc);
1319   }
1320 
1321   // make hash value unique in case we don't have one already
1322   if (kt.m_strBankID.isEmpty()) {
1323     QString hashBase;
1324     hashBase.sprintf("%s-%07lx", qPrintable(kt.m_datePosted.toString(Qt::ISODate)), h);
1325     int idx = 1;
1326     QString hash;
1327     for (;;) {
1328       hash = QString("%1-%2").arg(hashBase).arg(idx);
1329       QMap<QString, bool>::const_iterator it;
1330       it = m_hashMap.constFind(hash);
1331       if (it == m_hashMap.constEnd()) {
1332         m_hashMap[hash] = true;
1333         break;
1334       }
1335       ++idx;
1336     }
1337     kt.m_strBankID = QString("%1-%2").arg(acc.id()).arg(hash);
1338   }
1339 
1340   // store transaction
1341   ks.m_listTransactions += kt;
1342 }
1343 
1344 
_slToStatement(MyMoneyStatement & ks,const MyMoneyAccount & acc,const AB_SECURITY * sy)1345 void KBankingExt::_slToStatement(MyMoneyStatement &ks,
1346                                  const MyMoneyAccount& acc,
1347                                  const AB_SECURITY *sy)
1348 {
1349   MyMoneyFile* file = MyMoneyFile::instance();
1350   QString s;
1351   QString memo;
1352   const char *p;
1353   const AB_VALUE *val;
1354   const GWEN_TIME *ti;
1355   MyMoneyStatement::Security ksy;
1356   MyMoneyStatement::Price kpr;
1357   MyMoneyStatement::Transaction kt;
1358 
1359   // security name
1360   p = AB_Security_GetName(sy);
1361   if (p)
1362     ksy.m_strName = QString::fromUtf8(p);
1363 
1364   // security id
1365   p = AB_Security_GetUniqueId(sy);
1366   if (p) {
1367     ksy.m_strId = QString::fromUtf8(p);
1368     ksy.m_strSymbol = QString::fromUtf8(p);
1369     kpr.m_strSecurity = QString::fromUtf8(p);
1370   }
1371 
1372   // date
1373   ti = AB_Security_GetUnitPriceDate(sy);
1374   if (ti) {
1375     int year, month, day;
1376 
1377     if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) {
1378       kpr.m_date = QDate(year, month + 1, day);
1379     }
1380   }
1381 
1382   // value
1383   val = AB_Security_GetUnitPriceValue(sy);
1384   if (val)
1385     kpr.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val));
1386 
1387   // generate dummy booking in case online balance does not match
1388   MyMoneySecurity security;
1389   MyMoneyAccount sacc;
1390   foreach (const auto sAccount, file->account(acc.id()).accountList()) {
1391     sacc=file->account(sAccount);
1392     security=file->security(sacc.currencyId());
1393     if ((!ksy.m_strSymbol.isEmpty() && QString::compare(ksy.m_strSymbol, security.tradingSymbol(), Qt::CaseInsensitive) == 0) ||
1394 	(!ksy.m_strName.isEmpty() && QString::compare(ksy.m_strName, security.name(), Qt::CaseInsensitive) == 0)) {
1395       if (sacc.balance().toDouble() != AB_Value_GetValueAsDouble(AB_Security_GetUnits(sy))) {
1396 	qDebug("Creating dummy correction booking for '%s' with %f shares", qPrintable(security.tradingSymbol()),
1397 	       AB_Value_GetValueAsDouble(AB_Security_GetUnits(sy))-sacc.balance().toDouble());
1398 	kt.m_fees = MyMoneyMoney();
1399 	kt.m_strMemo = "Dummy booking added by KMyMoney to reflect online balance - please adjust";
1400 	kt.m_datePosted = QDate::currentDate();
1401 	kt.m_strSymbol=security.tradingSymbol();
1402 	kt.m_strSecurity=security.name();
1403 	kt.m_strBrokerageAccount=acc.name();
1404 
1405 	kt.m_shares=MyMoneyMoney(AB_Value_GetValueAsDouble(AB_Security_GetUnits(sy))-sacc.balance().toDouble());
1406 	if (AB_Value_GetValueAsDouble(AB_Security_GetUnits(sy)) > sacc.balance().toDouble())
1407 	  kt.m_eAction = eMyMoney::Transaction::Action::Shrsin;
1408 	else
1409 	  kt.m_eAction = eMyMoney::Transaction::Action::Shrsout;
1410 
1411 	// store transaction
1412 	ks.m_listTransactions += kt;
1413       }
1414       else
1415 	qDebug("Online balance matches balance in KMyMoney account!");
1416     }
1417   }
1418 
1419   // store security
1420   ks.m_listSecurities += ksy;
1421 
1422   // store prices
1423   ks.m_listPrices += kpr;
1424 }
1425 
1426 
importAccountInfo(AB_IMEXPORTER_CONTEXT * ctx,AB_IMEXPORTER_ACCOUNTINFO * ai,uint32_t)1427 bool KBankingExt::importAccountInfo(AB_IMEXPORTER_CONTEXT *ctx,
1428                                     AB_IMEXPORTER_ACCOUNTINFO *ai,
1429                                     uint32_t /*flags*/)
1430 {
1431   const char *p;
1432 
1433   DBG_INFO(0, "Importing account...");
1434 
1435   // account number
1436   MyMoneyStatement ks;
1437   p = AB_ImExporterAccountInfo_GetAccountNumber(ai);
1438   if (p) {
1439     ks.m_strAccountNumber = m_parent->stripLeadingZeroes(p);
1440   }
1441 
1442   p = AB_ImExporterAccountInfo_GetBankCode(ai);
1443   if (p) {
1444     ks.m_strRoutingNumber = m_parent->stripLeadingZeroes(p);
1445   }
1446 
1447   MyMoneyAccount kacc;
1448   if (!ks.m_strAccountNumber.isEmpty() || !ks.m_strRoutingNumber.isEmpty()) {
1449     kacc = m_parent->account("kbanking-acc-ref", QString("%1-%2-%3").arg(ks.m_strRoutingNumber, ks.m_strAccountNumber).arg(AB_ImExporterAccountInfo_GetAccountType(ai)));
1450     ks.m_accountId = kacc.id();
1451   }
1452 
1453   // account name
1454   p = AB_ImExporterAccountInfo_GetAccountName(ai);
1455   if (p)
1456     ks.m_strAccountName = p;
1457 
1458   // account type
1459   switch (AB_ImExporterAccountInfo_GetAccountType(ai)) {
1460     case AB_AccountType_Bank:
1461       ks.m_eType = eMyMoney::Statement::Type::Savings;
1462       break;
1463     case AB_AccountType_CreditCard:
1464       ks.m_eType = eMyMoney::Statement::Type::CreditCard;
1465       break;
1466     case AB_AccountType_Checking:
1467       ks.m_eType = eMyMoney::Statement::Type::Checkings;
1468       break;
1469     case AB_AccountType_Savings:
1470       ks.m_eType = eMyMoney::Statement::Type::Savings;
1471       break;
1472     case AB_AccountType_Investment:
1473       ks.m_eType = eMyMoney::Statement::Type::Investment;
1474       break;
1475     case AB_AccountType_Cash:
1476     default:
1477       ks.m_eType = eMyMoney::Statement::Type::None;
1478   }
1479 
1480   // account status
1481   const AB_BALANCE *bal = AB_Balance_List_GetLatestByType(AB_ImExporterAccountInfo_GetBalanceList(ai), AB_Balance_TypeBooked);
1482   if (!bal)
1483     bal = AB_Balance_List_GetLatestByType(AB_ImExporterAccountInfo_GetBalanceList(ai), AB_Balance_TypeNoted);
1484   if (bal) {
1485     const AB_VALUE* val = AB_Balance_GetValue(bal);
1486     if (val) {
1487       DBG_INFO(0, "Importing balance");
1488       ks.m_closingBalance = AB_Value_toMyMoneyMoney(val);
1489       p = AB_Value_GetCurrency(val);
1490       if (p)
1491         ks.m_strCurrency = p;
1492     }
1493     const GWEN_DATE* dt = AB_Balance_GetDate(bal);
1494     if (dt) {
1495       ks.m_dateEnd = QDate(GWEN_Date_GetYear(dt), GWEN_Date_GetMonth(dt) , GWEN_Date_GetDay(dt));
1496     } else {
1497       DBG_WARN(0, "No date for balance");
1498     }
1499   } else {
1500       DBG_WARN(0, "No account balance");
1501   }
1502 
1503   // clear hash map
1504   m_hashMap.clear();
1505 
1506   // get all securities
1507   const AB_SECURITY* s = AB_ImExporterContext_GetFirstSecurity(ctx);
1508   while (s) {
1509     qDebug("Found security '%s'", AB_Security_GetName(s));
1510     _slToStatement(ks, kacc, s);
1511     s = AB_Security_List_Next(s);
1512   }
1513 
1514   // get all transactions
1515   const AB_TRANSACTION* t = AB_ImExporterAccountInfo_GetFirstTransaction(ai, AB_Transaction_TypeStatement, 0);
1516   while (t) {
1517     _xaToStatement(ks, kacc, t);
1518     t = AB_Transaction_List_FindNextByType(t, AB_Transaction_TypeStatement, 0);
1519   }
1520 
1521   // import them
1522   if (!m_parent->importStatement(ks)) {
1523     if (KMessageBox::warningYesNo(0,
1524                                   i18n("Error importing statement. Do you want to continue?"),
1525                                   i18n("Critical Error")) == KMessageBox::No) {
1526       DBG_ERROR(0, "User aborted");
1527       return false;
1528     }
1529   }
1530   return true;
1531 }
1532 
1533 K_PLUGIN_FACTORY_WITH_JSON(KBankingFactory, "kbanking.json", registerPlugin<KBanking>();)
1534 
1535 #include "kbanking.moc"
1536