1 /*
2  * Copyright 2013-2014  Christian Dávid <christian-david@web.de>
3  * Copyright 2019       Thomas Baumgart <tbaumgart@kde.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of
8  * the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "konlinejoboutboxview.h"
20 
21 #include <memory>
22 
23 #include "ui_konlinejoboutboxview.h"
24 #include "kmymoneyviewbase_p.h"
25 #include "konlinetransferform.h"
26 #include "kmymoneyplugin.h"
27 
28 #include <KLocalizedString>
29 #include <KSharedConfig>
30 #include <KConfigGroup>
31 #include <QAction>
32 #include <QMenu>
33 #include <QTimer>
34 #include <QModelIndex>
35 #include <QModelIndexList>
36 #include <KMessageBox>
37 #include <KActionCollection>
38 
39 #include "models.h"
40 #include "onlinejobmodel.h"
41 #include "onlinejobadministration.h"
42 #include "onlinejobtyped.h"
43 #include "onlinejobmessagesview.h"
44 #include "onlinejobmessagesmodel.h"
45 #include "onlinepluginextended.h"
46 
47 #include "mymoneyaccount.h"
48 #include "mymoneyfile.h"
49 #include "menuenums.h"
50 #include "mymoneyenums.h"
51 #include "mymoneyexception.h"
52 
53 #include <QDebug>
54 
55 class KOnlineJobOutboxViewPrivate : public KMyMoneyViewBasePrivate
56 {
57   Q_DECLARE_PUBLIC(KOnlineJobOutboxView)
58 
59 public:
KOnlineJobOutboxViewPrivate(KOnlineJobOutboxView * qq)60   explicit KOnlineJobOutboxViewPrivate(KOnlineJobOutboxView *qq) :
61     KMyMoneyViewBasePrivate(),
62     q_ptr(qq),
63     ui(new Ui::KOnlineJobOutboxView),
64     m_needLoad(true),
65     m_onlinePlugins(nullptr),
66     m_onlineJobModel(nullptr)
67   {
68   }
69 
~KOnlineJobOutboxViewPrivate()70   ~KOnlineJobOutboxViewPrivate()
71   {
72     if (!m_needLoad) {
73       // Save column state
74       KConfigGroup configGroup = KSharedConfig::openConfig()->group("KOnlineJobOutboxView");
75       configGroup.writeEntry("HeaderState", ui->m_onlineJobView->header()->saveState());
76     }
77   }
78 
init()79   void init()
80   {
81     Q_Q(KOnlineJobOutboxView);
82     m_needLoad = false;
83     ui->setupUi(q);
84 
85     // Restore column state
86     KConfigGroup configGroup = KSharedConfig::openConfig()->group("KOnlineJobOutboxView");
87     QByteArray columns;
88     columns = configGroup.readEntry("HeaderState", columns);
89     ui->m_onlineJobView->header()->restoreState(columns);
90 
91     ui->m_onlineJobView->setModel(this->onlineJobsModel());
92     q->connect(ui->m_buttonSend, &QAbstractButton::clicked, q, &KOnlineJobOutboxView::slotSendJobs);
93     q->connect(ui->m_buttonRemove, &QAbstractButton::clicked, q, &KOnlineJobOutboxView::slotRemoveJob);
94     q->connect(ui->m_buttonEdit, &QAbstractButton::clicked, q, static_cast<void (KOnlineJobOutboxView::*)()>(&KOnlineJobOutboxView::slotEditJob));
95     q->connect(ui->m_onlineJobView, &QAbstractItemView::doubleClicked, q, static_cast<void (KOnlineJobOutboxView::*)(const QModelIndex &)>(&KOnlineJobOutboxView::slotEditJob));
96     q->connect(ui->m_onlineJobView->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KOnlineJobOutboxView::updateButtonState);
97 
98     // Set new credit transfer button
99     q->connect(pActions[eMenu::Action::AccountCreditTransfer], &QAction::changed, q, &KOnlineJobOutboxView::updateNewCreditTransferButton);
100     q->connect(ui->m_buttonNewCreditTransfer, &QAbstractButton::clicked, q, &KOnlineJobOutboxView::slotNewCreditTransfer);
101     q->updateNewCreditTransferButton();
102   }
103 
onlineJobsModel()104   onlineJobModel* onlineJobsModel()
105   {
106     Q_Q(KOnlineJobOutboxView);
107     if (!m_onlineJobModel) {
108       m_onlineJobModel = new onlineJobModel(q);
109   #ifdef KMM_MODELTEST
110       /// @todo using the ModelTest feature on the onlineJobModel crashes. Need to fix.
111       // new ModelTest(m_onlineJobModel, Models::instance());
112   #endif
113     }
114     return m_onlineJobModel;
115   }
116 
117 
editJob(const QString jobId)118   void editJob(const QString jobId)
119   {
120     try {
121       const onlineJob constJob = MyMoneyFile::instance()->getOnlineJob(jobId);
122       editJob(constJob);
123     } catch (const MyMoneyException &) {
124       // Prevent a crash in very rare cases
125     }
126   }
127 
editJob(onlineJob job)128   void editJob(onlineJob job)
129   {
130     try {
131       editJob(onlineJobTyped<creditTransfer>(job));
132     } catch (const MyMoneyException &) {
133     }
134   }
135 
editJob(const onlineJobTyped<creditTransfer> job)136   void editJob(const onlineJobTyped<creditTransfer> job)
137   {
138     Q_Q(KOnlineJobOutboxView);
139     auto transferForm = new kOnlineTransferForm(q);
140     transferForm->setOnlineJob(job);
141     q->connect(transferForm, &QDialog::rejected, transferForm, &QObject::deleteLater);
142     q->connect(transferForm, &kOnlineTransferForm::acceptedForSave, q, &KOnlineJobOutboxView::slotOnlineJobSave);
143     q->connect(transferForm, &kOnlineTransferForm::acceptedForSend, q, static_cast<void (KOnlineJobOutboxView::*)(onlineJob)>(&KOnlineJobOutboxView::slotOnlineJobSend));
144     q->connect(transferForm, &QDialog::accepted, transferForm, &QObject::deleteLater);
145     transferForm->show();
146   }
147 
148   KOnlineJobOutboxView     *q_ptr;
149   std::unique_ptr<Ui::KOnlineJobOutboxView> ui;
150 
151   /**
152     * This member holds the load state of page
153     */
154   bool m_needLoad;
155   QMap<QString, KMyMoneyPlugin::OnlinePlugin*>* m_onlinePlugins;
156   onlineJobModel *m_onlineJobModel;
157   MyMoneyAccount m_currentAccount;
158 };
159 
KOnlineJobOutboxView(QWidget * parent)160 KOnlineJobOutboxView::KOnlineJobOutboxView(QWidget *parent) :
161   KMyMoneyViewBase(*new KOnlineJobOutboxViewPrivate(this), parent)
162 {
163   connect(pActions[eMenu::Action::LogOnlineJob], &QAction::triggered, this, static_cast<void (KOnlineJobOutboxView::*)()>(&KOnlineJobOutboxView::slotOnlineJobLog));
164   connect(pActions[eMenu::Action::AccountCreditTransfer], &QAction::triggered, this, &KOnlineJobOutboxView::slotNewCreditTransfer);
165 }
166 
~KOnlineJobOutboxView()167 KOnlineJobOutboxView::~KOnlineJobOutboxView()
168 {
169 }
170 
updateButtonState() const171 void KOnlineJobOutboxView::updateButtonState() const
172 {
173   Q_D(const KOnlineJobOutboxView);
174   const QModelIndexList indexes = d->ui->m_onlineJobView->selectionModel()->selectedRows();
175   const int selectedItems = indexes.count();
176 
177   // Send button
178   //! @todo Enable button if it is useful
179   //ui->m_buttonSend->setEnabled(selectedItems > 0);
180 
181   // Edit button/action
182   bool editable = true;
183   QString tooltip;
184 
185   if (selectedItems == 1) {
186     const onlineJob job = d->ui->m_onlineJobView->model()->data(indexes.first(), onlineJobModel::OnlineJobRole).value<onlineJob>();
187 
188     if (!job.isEditable()) {
189       editable = false;
190       if (job.sendDate().isValid()) {
191         /// @todo maybe add a word about unable to edit but able to copy here
192         // I don't do it right away since we are in string freeze for 5.0.7
193         tooltip = i18n("This job cannot be edited anymore because it was sent already.");
194         editable = true;
195       } else if (job.isLocked())
196         tooltip = i18n("Job is being processed at the moment.");
197       else
198         Q_ASSERT(false);
199     } else if (!onlineJobAdministration::instance()->canEditOnlineJob(job)) {
200       editable = false;
201       tooltip = i18n("The plugin to edit this job is not available.");
202     }
203   } else {
204     editable = false;
205     tooltip = i18n("You need to select a single job for editing.");
206   }
207 
208   QAction *const onlinejob_edit = pActions[eMenu::Action::EditOnlineJob];
209   Q_CHECK_PTR(onlinejob_edit);
210   onlinejob_edit->setEnabled(editable);
211   onlinejob_edit->setToolTip(tooltip);
212 
213   d->ui->m_buttonEdit->setEnabled(editable);
214   d->ui->m_buttonEdit->setToolTip(tooltip);
215 
216   // Delete button/action
217   QAction *const onlinejob_delete = pActions[eMenu::Action::DeleteOnlineJob];
218   Q_CHECK_PTR(onlinejob_delete);
219   onlinejob_delete->setEnabled(selectedItems > 0);
220   d->ui->m_buttonRemove->setEnabled(onlinejob_delete->isEnabled());
221 }
222 
updateNewCreditTransferButton()223 void KOnlineJobOutboxView::updateNewCreditTransferButton()
224 {
225   Q_D(KOnlineJobOutboxView);
226   auto action = pActions[eMenu::Action::AccountCreditTransfer];
227   Q_CHECK_PTR(action);
228   d->ui->m_buttonNewCreditTransfer->setEnabled(action->isEnabled());
229 }
230 
slotRemoveJob()231 void KOnlineJobOutboxView::slotRemoveJob()
232 {
233   Q_D(KOnlineJobOutboxView);
234   QAbstractItemModel* model = d->ui->m_onlineJobView->model();
235   QModelIndexList indexes = d->ui->m_onlineJobView->selectionModel()->selectedRows();
236 
237   while (!indexes.isEmpty()) {
238     model->removeRow(indexes.at(0).row());
239     indexes = d->ui->m_onlineJobView->selectionModel()->selectedRows();
240   }
241 }
242 
selectedOnlineJobs() const243 QStringList KOnlineJobOutboxView::selectedOnlineJobs() const
244 {
245   Q_D(const KOnlineJobOutboxView);
246   QModelIndexList indexes = d->ui->m_onlineJobView->selectionModel()->selectedRows();
247 
248   if (indexes.isEmpty())
249     return QStringList();
250 
251   QStringList list;
252   list.reserve(indexes.count());
253 
254   const QAbstractItemModel *const model = d->ui->m_onlineJobView->model();
255   Q_FOREACH(const QModelIndex& index, indexes) {
256     list.append(model->data(index, onlineJobModel::OnlineJobId).toString());
257   }
258   return list;
259 }
260 
slotSelectByObject(const MyMoneyObject & obj,eView::Intent intent)261 void KOnlineJobOutboxView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent)
262 {
263   switch(intent) {
264     case eView::Intent::UpdateActions:
265       updateActions(obj);
266       break;
267 
268     default:
269       break;
270   }
271 }
272 
slotSelectByVariant(const QVariantList & variant,eView::Intent intent)273 void KOnlineJobOutboxView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent)
274 {
275   Q_D(KOnlineJobOutboxView);
276   switch(intent) {
277     case eView::Intent::SetOnlinePlugins:
278       if (variant.count() == 1)
279         d->m_onlinePlugins = static_cast<QMap<QString, KMyMoneyPlugin::OnlinePlugin*>*>(variant.first().value<void*>());
280       break;
281     default:
282       break;
283   }
284 }
285 
slotSendJobs()286 void KOnlineJobOutboxView::slotSendJobs()
287 {
288   Q_D(KOnlineJobOutboxView);
289   if (d->ui->m_onlineJobView->selectionModel()->hasSelection())
290     slotSendSelectedJobs();
291   else
292     slotSendAllSendableJobs();
293 }
294 
slotSendAllSendableJobs()295 void KOnlineJobOutboxView::slotSendAllSendableJobs()
296 {
297   QList<onlineJob> validJobs;
298   foreach (const onlineJob& job, MyMoneyFile::instance()->onlineJobList()) {
299     if (job.isValid() && job.isEditable())
300       validJobs.append(job);
301   }
302   qDebug() << "I shall send " << validJobs.count() << "/" << MyMoneyFile::instance()->onlineJobList().count() << " onlineJobs";
303   if (!validJobs.isEmpty())
304     slotOnlineJobSend(validJobs);
305 //    emit sendJobs(validJobs);
306 }
307 
slotSendSelectedJobs()308 void KOnlineJobOutboxView::slotSendSelectedJobs()
309 {
310   Q_D(KOnlineJobOutboxView);
311   QModelIndexList indexes = d->ui->m_onlineJobView->selectionModel()->selectedRows();
312   if (indexes.isEmpty())
313     return;
314 
315   // Valid jobs to send
316   QList<onlineJob> validJobs;
317   validJobs.reserve(indexes.count());
318 
319   // Get valid jobs
320   const QAbstractItemModel *const model = d->ui->m_onlineJobView->model();
321   foreach (const QModelIndex& index, indexes) {
322     onlineJob job = model->data(index, onlineJobModel::OnlineJobRole).value<onlineJob>();
323     if (job.isValid() && job.isEditable())
324       validJobs.append(job);
325   }
326 
327   // Abort if not all jobs can be sent
328   if (validJobs.count() != indexes.count()) {
329     KMessageBox::information(this, i18nc("The user selected credit transfers to send. But they cannot be sent.",
330                                          "Cannot send selection"),
331                              i18n("Not all selected credit transfers can be sent because some of them are invalid or were already sent."));
332     return;
333   }
334 
335   slotOnlineJobSend(validJobs);
336 //  emit sendJobs(validJobs);
337 }
338 
slotEditJob()339 void KOnlineJobOutboxView::slotEditJob()
340 {
341   Q_D(KOnlineJobOutboxView);
342   QModelIndexList indexes = d->ui->m_onlineJobView->selectionModel()->selectedIndexes();
343   if (!indexes.isEmpty()) {
344     QString jobId = d->ui->m_onlineJobView->model()->data(indexes.first(), onlineJobModel::OnlineJobId).toString();
345     Q_ASSERT(!jobId.isEmpty());
346     d->editJob(jobId);
347 //    emit editJob(jobId);
348   }
349 }
350 
slotEditJob(const QModelIndex & index)351 void KOnlineJobOutboxView::slotEditJob(const QModelIndex &index)
352 {
353   if (!pActions[eMenu::Action::EditOnlineJob]->isEnabled())
354     return;
355 
356   Q_D(KOnlineJobOutboxView);
357   auto jobId = d->ui->m_onlineJobView->model()->data(index, onlineJobModel::OnlineJobId).toString();
358   d->editJob(jobId);
359 //  emit editJob(jobId);
360 }
361 
contextMenuEvent(QContextMenuEvent *)362 void KOnlineJobOutboxView::contextMenuEvent(QContextMenuEvent*)
363 {
364   if (!pActions[eMenu::Action::EditOnlineJob]->isEnabled())
365     return;
366 
367   Q_D(KOnlineJobOutboxView);
368   QModelIndexList indexes = d->ui->m_onlineJobView->selectionModel()->selectedIndexes();
369   if (!indexes.isEmpty()) {
370 //    onlineJob job = d->ui->m_onlineJobView->model()->data(indexes.first(), onlineJobModel::OnlineJobRole).value<onlineJob>();
371     pMenus[eMenu::Menu::OnlineJob]->exec(QCursor::pos());
372   }
373 }
374 
375 /**
376  * Do not know why this is needed, but all other views in KMyMoney have it.
377  */
showEvent(QShowEvent * event)378 void KOnlineJobOutboxView::showEvent(QShowEvent* event)
379 {
380   Q_D(KOnlineJobOutboxView);
381   if (d->m_needLoad)
382     d->init();
383 
384   emit customActionRequested(View::OnlineJobOutbox, eView::Action::AboutToShow);
385   // don't forget base class implementation
386   QWidget::showEvent(event);
387 }
388 
executeCustomAction(eView::Action action)389 void KOnlineJobOutboxView::executeCustomAction(eView::Action action)
390 {
391   Q_D(KOnlineJobOutboxView);
392   switch(action) {
393     case eView::Action::SetDefaultFocus:
394       {
395         Q_D(KOnlineJobOutboxView);
396         QTimer::singleShot(0, d->ui->m_onlineJobView, SLOT(setFocus()));
397       }
398       break;
399 
400     case eView::Action::InitializeAfterFileOpen:
401       d->onlineJobsModel()->load();
402       break;
403 
404     case eView::Action::CleanupBeforeFileClose:
405       d->onlineJobsModel()->unload();
406       break;
407 
408     default:
409       break;
410   }
411 }
412 
updateActions(const MyMoneyObject & obj)413 void KOnlineJobOutboxView::updateActions(const MyMoneyObject& obj)
414 {
415   Q_D(KOnlineJobOutboxView);
416   if (typeid(obj) != typeid(MyMoneyAccount) &&
417       (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled)))
418     return;
419 
420   const auto& acc = static_cast<const MyMoneyAccount&>(obj);
421   d->m_currentAccount = acc;
422 }
423 
slotOnlineJobSave(onlineJob job)424 void KOnlineJobOutboxView::slotOnlineJobSave(onlineJob job)
425 {
426   MyMoneyFileTransaction fileTransaction;
427   if (job.id().isEmpty())
428     MyMoneyFile::instance()->addOnlineJob(job);
429   else
430     MyMoneyFile::instance()->modifyOnlineJob(job);
431   fileTransaction.commit();
432 }
433 
434 /** @todo when onlineJob queue is used, continue here */
slotOnlineJobSend(onlineJob job)435 void KOnlineJobOutboxView::slotOnlineJobSend(onlineJob job)
436 {
437   MyMoneyFileTransaction fileTransaction;
438   if (job.id().isEmpty())
439     MyMoneyFile::instance()->addOnlineJob(job);
440   else
441     MyMoneyFile::instance()->modifyOnlineJob(job);
442   fileTransaction.commit();
443 
444   QList<onlineJob> jobList;
445   jobList.append(job);
446   slotOnlineJobSend(jobList);
447 }
448 
slotOnlineJobSend(QList<onlineJob> jobs)449 void KOnlineJobOutboxView::slotOnlineJobSend(QList<onlineJob> jobs)
450 {
451   Q_D(KOnlineJobOutboxView);
452   MyMoneyFile *const kmmFile = MyMoneyFile::instance();
453   QMultiMap<QString, onlineJob> jobsByPlugin;
454 
455   // Sort jobs by online plugin & lock them
456   foreach (onlineJob job, jobs) {
457     Q_ASSERT(!job.id().isEmpty());
458     // find the provider
459     const MyMoneyAccount originAcc = job.responsibleMyMoneyAccount();
460     job.setLock();
461     job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KMyMoneyApp::slotOnlineJobSend", "Added to queue for plugin '" + originAcc.onlineBankingSettings().value("provider").toLower() + '\''));
462     MyMoneyFileTransaction fileTransaction;
463     kmmFile->modifyOnlineJob(job);
464     fileTransaction.commit();
465     jobsByPlugin.insert(originAcc.onlineBankingSettings().value("provider").toLower(), job);
466   }
467 
468   // Send onlineJobs to plugins
469   QList<QString> usedPlugins = jobsByPlugin.keys();
470   std::sort(usedPlugins.begin(), usedPlugins.end());
471   const QList<QString>::iterator newEnd = std::unique(usedPlugins.begin(), usedPlugins.end());
472   usedPlugins.erase(newEnd, usedPlugins.end());
473 
474   foreach (const QString& pluginKey, usedPlugins) {
475     QMap<QString, KMyMoneyPlugin::OnlinePlugin*>::const_iterator it_p = d->m_onlinePlugins->constFind(pluginKey);
476 
477     if (it_p != d->m_onlinePlugins->constEnd()) {
478       // plugin found, call it
479       KMyMoneyPlugin::OnlinePluginExtended *pluginExt = dynamic_cast< KMyMoneyPlugin::OnlinePluginExtended* >(*it_p);
480       if (pluginExt == 0) {
481         qWarning("Job given for plugin which is not an extended plugin");
482         continue;
483       }
484       //! @fixme remove debug message
485       qDebug() << "Sending " << jobsByPlugin.count(pluginKey) << " job(s) to online plugin " << pluginKey;
486       QList<onlineJob> jobsToExecute = jobsByPlugin.values(pluginKey);
487       QList<onlineJob> executedJobs = jobsToExecute;
488       pluginExt->sendOnlineJob(executedJobs);
489 
490       // Save possible changes of the online job and remove lock
491       MyMoneyFileTransaction fileTransaction;
492       foreach (onlineJob job, executedJobs) {
493         fileTransaction.restart();
494         job.setLock(false);
495         kmmFile->modifyOnlineJob(job);
496         fileTransaction.commit();
497       }
498 
499       if (Q_UNLIKELY(executedJobs.size() != jobsToExecute.size())) {
500         // OnlinePlugin did not return all jobs
501         qWarning() << "Error saving send online tasks. After restart you should see at minimum all successfully executed jobs marked send. Imperfect plugin: " << pluginExt->objectName();
502       }
503 
504     } else {
505       qWarning() << "Error, got onlineJob for an account without online plugin.";
506       /** @FIXME can this actually happen? */
507     }
508   }
509 }
510 
slotOnlineJobLog()511 void KOnlineJobOutboxView::slotOnlineJobLog()
512 {
513   QStringList jobIds = this->selectedOnlineJobs();
514   slotOnlineJobLog(jobIds);
515 }
516 
slotOnlineJobLog(const QStringList & onlineJobIds)517 void KOnlineJobOutboxView::slotOnlineJobLog(const QStringList& onlineJobIds)
518 {
519   onlineJobMessagesView *const dialog = new onlineJobMessagesView();
520   onlineJobMessagesModel *const model = new onlineJobMessagesModel(dialog);
521   model->setOnlineJob(MyMoneyFile::instance()->getOnlineJob(onlineJobIds.first()));
522   dialog->setModel(model);
523   dialog->setAttribute(Qt::WA_DeleteOnClose);
524   dialog->show();
525   // Note: Objects are not deleted here, Qt's parent-child system has to do that.
526 }
527 
slotNewCreditTransfer()528 void KOnlineJobOutboxView::slotNewCreditTransfer()
529 {
530   Q_D(KOnlineJobOutboxView);
531   auto *transferForm = new kOnlineTransferForm(this);
532   if (!d->m_currentAccount.id().isEmpty()) {
533     transferForm->setCurrentAccount(d->m_currentAccount.id());
534   }
535   connect(transferForm, &QDialog::rejected, transferForm, &QObject::deleteLater);
536   connect(transferForm, &kOnlineTransferForm::acceptedForSave, this, &KOnlineJobOutboxView::slotOnlineJobSave);
537   connect(transferForm, &kOnlineTransferForm::acceptedForSend, this, static_cast<void (KOnlineJobOutboxView::*)(onlineJob)>(&KOnlineJobOutboxView::slotOnlineJobSend));
538   connect(transferForm, &QDialog::accepted, transferForm, &QObject::deleteLater);
539   transferForm->show();
540 }
541