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