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