1 /*
2  * Copyright 2004-2017  Thomas Baumgart <tbaumgart@kde.org>
3  * Copyright 2004       Kevin Tambascio <ktambascio@users.sourceforge.net>
4  * Copyright 2004-2006  Ace Jones <acejones@users.sourceforge.net>
5  * Copyright 2017-2018  Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
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 "kequitypriceupdatedlg.h"
22 
23 // ----------------------------------------------------------------------------
24 // QT Includes
25 
26 #include <QPushButton>
27 #include <QTimer>
28 #include <QList>
29 #include <QPointer>
30 
31 // ----------------------------------------------------------------------------
32 // KDE Includes
33 
34 #include <KMessageBox>
35 #include <KTextEdit>
36 #include <KConfig>
37 #include <KConfigGroup>
38 #include <KSharedConfig>
39 #include <KStandardGuiItem>
40 #include <KLocalizedString>
41 #include <KColorScheme>
42 
43 // ----------------------------------------------------------------------------
44 // Project Includes
45 
46 #include "ui_kequitypriceupdatedlg.h"
47 
48 #include "mymoneyfile.h"
49 #include "mymoneyaccount.h"
50 #include "mymoneysecurity.h"
51 #include "mymoneyprice.h"
52 #include "webpricequote.h"
53 #include "kequitypriceupdateconfdlg.h"
54 #include "kmymoneyutils.h"
55 #include "mymoneyexception.h"
56 #include "dialogenums.h"
57 
58 #define WEBID_COL       0
59 #define NAME_COL        1
60 #define PRICE_COL       2
61 #define DATE_COL        3
62 #define KMMID_COL       4
63 #define SOURCE_COL      5
64 
65 class KEquityPriceUpdateDlgPrivate
66 {
67   Q_DISABLE_COPY(KEquityPriceUpdateDlgPrivate)
68   Q_DECLARE_PUBLIC(KEquityPriceUpdateDlg)
69 
70 public:
KEquityPriceUpdateDlgPrivate(KEquityPriceUpdateDlg * qq)71   explicit KEquityPriceUpdateDlgPrivate(KEquityPriceUpdateDlg *qq) :
72     q_ptr(qq),
73     ui(new Ui::KEquityPriceUpdateDlg),
74     m_fUpdateAll(false),
75     m_updatingPricePolicy(eDialogs::UpdatePrice::All)
76   {
77   }
78 
~KEquityPriceUpdateDlgPrivate()79   ~KEquityPriceUpdateDlgPrivate()
80   {
81     delete ui;
82   }
83 
init(const QString & securityId)84   void init(const QString& securityId)
85   {
86     Q_Q(KEquityPriceUpdateDlg);
87     ui->setupUi(q);
88     m_fUpdateAll = false;
89     QStringList headerList;
90     headerList << i18n("ID") << i18nc("Equity name", "Name")
91     << i18n("Price") << i18n("Date");
92 
93     ui->lvEquityList->header()->setSortIndicator(0, Qt::AscendingOrder);
94     ui->lvEquityList->setColumnWidth(NAME_COL, 125);
95 
96     // This is a "get it up and running" hack.  Will replace this in the future.
97     headerList << i18nc("Internal identifier", "Internal ID")
98     << i18nc("Online quote source", "Source");
99     ui->lvEquityList->setColumnWidth(KMMID_COL, 0);
100 
101     ui->lvEquityList->setHeaderLabels(headerList);
102 
103     ui->lvEquityList->setSelectionMode(QAbstractItemView::MultiSelection);
104     ui->lvEquityList->setAllColumnsShowFocus(true);
105 
106     ui->btnUpdateAll->setEnabled(false);
107 
108     auto file = MyMoneyFile::instance();
109 
110     //
111     // Add each price pair that we know about
112     //
113 
114     // send in securityId == "XXX YYY" to get a single-shot update for XXX to YYY.
115     // for consistency reasons, this accepts the same delimiters as WebPriceQuote::launch()
116     QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive);
117     MyMoneySecurityPair currencyIds;
118     if (splitrx.indexIn(securityId) != -1) {
119       currencyIds = MyMoneySecurityPair(splitrx.cap(1), splitrx.cap(2));
120     }
121 
122     MyMoneyPriceList prices = file->priceList();
123     for (MyMoneyPriceList::ConstIterator it_price = prices.constBegin(); it_price != prices.constEnd(); ++it_price) {
124       const MyMoneySecurityPair& pair = it_price.key();
125       if (file->security(pair.first).isCurrency() && (securityId.isEmpty() || (pair == currencyIds))) {
126         const MyMoneyPriceEntries& entries = (*it_price);
127         if (entries.count() > 0 && entries.begin().key() <= QDate::currentDate()) {
128           addPricePair(pair, false);
129           ui->btnUpdateAll->setEnabled(true);
130         }
131       }
132     }
133 
134     //
135     // Add each investment
136     //
137 
138     QList<MyMoneySecurity> securities = file->securityList();
139     for (QList<MyMoneySecurity>::const_iterator it = securities.constBegin(); it != securities.constEnd(); ++it) {
140       if (!(*it).isCurrency()
141           && (securityId.isEmpty() || ((*it).id() == securityId))
142           && !(*it).value("kmm-online-source").isEmpty()
143          ) {
144         addInvestment(*it);
145         ui->btnUpdateAll->setEnabled(true);
146       }
147     }
148 
149     // if list is empty and a price pair has been requested, add it
150     if (ui->lvEquityList->invisibleRootItem()->childCount() == 0 && !securityId.isEmpty()) {
151       addPricePair(currencyIds, true);
152     }
153 
154     q->connect(ui->btnUpdateSelected, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotUpdateSelectedClicked);
155     q->connect(ui->btnUpdateAll, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotUpdateAllClicked);
156 
157     q->connect(ui->m_fromDate, &KMyMoneyDateInput::dateChanged, q, &KEquityPriceUpdateDlg::slotDateChanged);
158     q->connect(ui->m_toDate, &KMyMoneyDateInput::dateChanged, q, &KEquityPriceUpdateDlg::slotDateChanged);
159 
160     q->connect(&m_webQuote, &WebPriceQuote::csvquote,
161             q, &KEquityPriceUpdateDlg::slotReceivedCSVQuote);
162     q->connect(&m_webQuote, &WebPriceQuote::quote,
163             q, &KEquityPriceUpdateDlg::slotReceivedQuote);
164     q->connect(&m_webQuote, &WebPriceQuote::failed,
165             q, &KEquityPriceUpdateDlg::slotQuoteFailed);
166     q->connect(&m_webQuote, &WebPriceQuote::status,
167             q, &KEquityPriceUpdateDlg::logStatusMessage);
168     q->connect(&m_webQuote, &WebPriceQuote::error,
169             q, &KEquityPriceUpdateDlg::logErrorMessage);
170 
171     q->connect(ui->lvEquityList, &QTreeWidget::itemSelectionChanged, q, &KEquityPriceUpdateDlg::slotUpdateSelection);
172 
173     q->connect(ui->btnConfigure, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotConfigureClicked);
174 
175     if (!securityId.isEmpty()) {
176       ui->btnUpdateSelected->hide();
177       ui->btnUpdateAll->hide();
178       // delete layout1;
179 
180       QTimer::singleShot(100, q, SLOT(slotUpdateAllClicked()));
181     }
182 
183     // Hide OK button until we have received the first update
184     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
185     if (ui->lvEquityList->invisibleRootItem()->childCount() == 0) {
186       ui->btnUpdateAll->setEnabled(false);
187     }
188     q->slotUpdateSelection();
189 
190     // previous versions of this dialog allowed to store a "Don't ask again" switch.
191     // Since we don't support it anymore, we just get rid of it
192     KSharedConfigPtr config = KSharedConfig::openConfig();
193     KConfigGroup grp = config->group("Notification Messages");
194     grp.deleteEntry("KEquityPriceUpdateDlg::slotQuoteFailed::Price Update Failed");
195     grp.sync();
196     grp = config->group("Equity Price Update");
197     int policyValue = grp.readEntry("PriceUpdatingPolicy", (int)eDialogs::UpdatePrice::Missing);
198     if (policyValue > (int)eDialogs::UpdatePrice::Ask || policyValue < (int)eDialogs::UpdatePrice::All)
199       m_updatingPricePolicy = eDialogs::UpdatePrice::Missing;
200     else
201       m_updatingPricePolicy = static_cast<eDialogs::UpdatePrice>(policyValue);
202   }
203 
addPricePair(const MyMoneySecurityPair & pair,bool dontCheckExistance)204   void addPricePair(const MyMoneySecurityPair& pair, bool dontCheckExistance)
205   {
206     auto file = MyMoneyFile::instance();
207 
208     const auto symbol = QString::fromLatin1("%1 > %2").arg(pair.first, pair.second);
209     const auto id = QString::fromLatin1("%1 %2").arg(pair.first, pair.second);
210     // Check that the pair does not already exist
211     if (ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL).empty()) {
212       const MyMoneyPrice &pr = file->price(pair.first, pair.second);
213       if (pr.source() != QLatin1String("KMyMoney")) {
214         bool keep = true;
215         if ((pair.first == file->baseCurrency().id())
216             || (pair.second == file->baseCurrency().id())) {
217           const QString& foreignCurrency = file->foreignCurrency(pair.first, pair.second);
218           // check that the foreign currency is still in use
219           QList<MyMoneyAccount>::const_iterator it_a;
220           QList<MyMoneyAccount> list;
221           file->accountList(list);
222           for (it_a = list.constBegin(); !dontCheckExistance && it_a != list.constEnd(); ++it_a) {
223             // if it's an account denominated in the foreign currency
224             // keep it
225             if (((*it_a).currencyId() == foreignCurrency)
226                 && !(*it_a).isClosed())
227               break;
228             // if it's an investment traded in the foreign currency
229             // keep it
230             if ((*it_a).isInvest() && !(*it_a).isClosed()) {
231               MyMoneySecurity sec = file->security((*it_a).currencyId());
232               if (sec.tradingCurrency() == foreignCurrency)
233                 break;
234             }
235           }
236           // if it is in use, it_a is not equal to list.end()
237           if (it_a == list.constEnd() && !dontCheckExistance)
238             keep = false;
239         }
240 
241         if (keep) {
242           auto item = new QTreeWidgetItem();
243           item->setText(WEBID_COL, symbol);
244           item->setText(NAME_COL, i18n("%1 units in %2", pair.first, pair.second));
245           if (pr.isValid()) {
246             MyMoneySecurity fromCurrency = file->currency(pair.second);
247             MyMoneySecurity toCurrency = file->currency(pair.first);
248             item->setText(PRICE_COL, pr.rate(pair.second).formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision()));
249             item->setText(DATE_COL, pr.date().toString(Qt::ISODate));
250           }
251           item->setText(KMMID_COL, id);
252           item->setText(SOURCE_COL, "KMyMoney Currency");  // This string value should not be localized
253           ui->lvEquityList->invisibleRootItem()->addChild(item);
254         }
255       }
256     }
257   }
258 
addInvestment(const MyMoneySecurity & inv)259   void addInvestment(const MyMoneySecurity& inv)
260   {
261     const auto id = inv.id();
262     // Check that the pair does not already exist
263     if (ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL).empty()) {
264       auto file = MyMoneyFile::instance();
265       // check that the security is still in use
266       QList<MyMoneyAccount>::const_iterator it_a;
267       QList<MyMoneyAccount> list;
268       file->accountList(list);
269       for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) {
270         if ((*it_a).isInvest()
271             && ((*it_a).currencyId() == inv.id())
272             && !(*it_a).isClosed())
273           break;
274       }
275       // if it is in use, it_a is not equal to list.end()
276       if (it_a != list.constEnd()) {
277         QString webID;
278         WebPriceQuoteSource onlineSource(inv.value("kmm-online-source"));
279         if (onlineSource.m_webIDBy == WebPriceQuoteSource::identifyBy::IdentificationNumber)
280           webID = inv.value("kmm-security-id");   // insert ISIN number...
281         else if (onlineSource.m_webIDBy == WebPriceQuoteSource::identifyBy::Name)
282           webID = inv.name();                     // ...or name...
283         else
284           webID = inv.tradingSymbol();            // ...or symbol
285 
286         QTreeWidgetItem* item = new QTreeWidgetItem();
287         item->setForeground(WEBID_COL, KColorScheme(QPalette::Normal).foreground(KColorScheme::NormalText));
288         if (webID.isEmpty()) {
289           webID = i18n("[No identifier]");
290           item->setForeground(WEBID_COL, KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText));
291         }
292         item->setText(WEBID_COL, webID);
293         item->setText(NAME_COL, inv.name());
294         MyMoneySecurity currency = file->currency(inv.tradingCurrency());
295         const MyMoneyPrice &pr = file->price(id.toUtf8(), inv.tradingCurrency());
296         if (pr.isValid()) {
297           item->setText(PRICE_COL, pr.rate(currency.id()).formatMoney(currency.tradingSymbol(), inv.pricePrecision()));
298           item->setText(DATE_COL, pr.date().toString(Qt::ISODate));
299         }
300         item->setText(KMMID_COL, id);
301         if (inv.value("kmm-online-quote-system") == "Finance::Quote")
302           item->setText(SOURCE_COL, QString("Finance::Quote %1").arg(inv.value("kmm-online-source")));
303         else
304           item->setText(SOURCE_COL, inv.value("kmm-online-source"));
305 
306         ui->lvEquityList->invisibleRootItem()->addChild(item);
307 
308         // If this investment is denominated in a foreign currency, ensure that
309         // the appropriate price pair is also on the list
310 
311         if (currency.id() != file->baseCurrency().id()) {
312           addPricePair(MyMoneySecurityPair(currency.id(), file->baseCurrency().id()), false);
313         }
314       }
315     }
316   }
317 
318   KEquityPriceUpdateDlg      *q_ptr;
319   Ui::KEquityPriceUpdateDlg  *ui;
320   bool                        m_fUpdateAll;
321   eDialogs::UpdatePrice        m_updatingPricePolicy;
322   WebPriceQuote               m_webQuote;
323 };
324 
KEquityPriceUpdateDlg(QWidget * parent,const QString & securityId)325 KEquityPriceUpdateDlg::KEquityPriceUpdateDlg(QWidget *parent, const QString& securityId) :
326   QDialog(parent),
327   d_ptr(new KEquityPriceUpdateDlgPrivate(this))
328 {
329   Q_D(KEquityPriceUpdateDlg);
330   d->init(securityId);
331 }
332 
~KEquityPriceUpdateDlg()333 KEquityPriceUpdateDlg::~KEquityPriceUpdateDlg()
334 {
335   Q_D(KEquityPriceUpdateDlg);
336   auto config = KSharedConfig::openConfig();
337   auto grp = config->group("Equity Price Update");
338   grp.writeEntry("PriceUpdatingPolicy", static_cast<int>(d->m_updatingPricePolicy));
339   grp.sync();
340   delete d;
341 }
342 
logErrorMessage(const QString & message)343 void KEquityPriceUpdateDlg::logErrorMessage(const QString& message)
344 {
345   logStatusMessage(QString("<font color=\"red\"><b>") + message + QString("</b></font>"));
346 }
347 
logStatusMessage(const QString & message)348 void KEquityPriceUpdateDlg::logStatusMessage(const QString& message)
349 {
350   Q_D(KEquityPriceUpdateDlg);
351   d->ui->lbStatus->append(message);
352 }
353 
price(const QString & id) const354 MyMoneyPrice KEquityPriceUpdateDlg::price(const QString& id) const
355 {
356   Q_D(const KEquityPriceUpdateDlg);
357   MyMoneyPrice price;
358   QTreeWidgetItem* item = nullptr;
359   QList<QTreeWidgetItem*> foundItems = d->ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL);
360 
361   if (! foundItems.empty())
362     item = foundItems.at(0);
363 
364   if (item) {
365     MyMoneyMoney rate(item->text(PRICE_COL));
366     if (!rate.isZero()) {
367       QString kmm_id = item->text(KMMID_COL).toUtf8();
368 
369       // if the ID has a space, then this is TWO ID's, so it's a currency quote
370       if (kmm_id.contains(" ")) {
371         QStringList ids = kmm_id.split(' ', QString::SkipEmptyParts);
372         QString fromid = ids[0].toUtf8();
373         QString toid = ids[1].toUtf8();
374         price = MyMoneyPrice(fromid, toid, QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL));
375       } else
376         // otherwise, it's a security quote
377       {
378         MyMoneySecurity security = MyMoneyFile::instance()->security(kmm_id);
379         price = MyMoneyPrice(kmm_id, security.tradingCurrency(), QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL));
380       }
381     }
382   }
383   return price;
384 }
385 
storePrices()386 void KEquityPriceUpdateDlg::storePrices()
387 {
388   Q_D(KEquityPriceUpdateDlg);
389   // update the new prices into the equities
390 
391   auto file = MyMoneyFile::instance();
392   QString name;
393 
394   MyMoneyFileTransaction ft;
395   try {
396     for (auto i = 0; i < d->ui->lvEquityList->invisibleRootItem()->childCount(); ++i) {
397       QTreeWidgetItem* item = d->ui->lvEquityList->invisibleRootItem()->child(i);
398       // turn on signals before we modify the last entry in the list
399       file->blockSignals(i < d->ui->lvEquityList->invisibleRootItem()->childCount() - 1);
400 
401       MyMoneyMoney rate(item->text(PRICE_COL));
402       if (!rate.isZero()) {
403         QString id = item->text(KMMID_COL);
404         QString fromid;
405         QString toid;
406 
407         // if the ID has a space, then this is TWO ID's, so it's a currency quote
408         if (id.contains(QLatin1Char(' '))) {
409           QStringList ids = id.split(QLatin1Char(' '), QString::SkipEmptyParts);
410           fromid = ids.at(0);
411           toid = ids.at(1);
412           name = QString::fromLatin1("%1 --> %2").arg(fromid, toid);
413         } else { // otherwise, it's a security quote
414           MyMoneySecurity security = file->security(id);
415           name = security.name();
416           fromid = id;
417           toid = security.tradingCurrency();
418         }
419         // TODO (Ace) Better handling of the case where there is already a price
420         // for this date.  Currently, it just overrides the old value.  Really it
421         // should check to see if the price is the same and prompt the user.
422         file->addPrice(MyMoneyPrice(fromid, toid, QDate::fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL)));
423       }
424     }
425     ft.commit();
426 
427   } catch (const MyMoneyException &) {
428     qDebug("Unable to add price information for %s", qPrintable(name));
429   }
430 }
431 
slotConfigureClicked()432 void KEquityPriceUpdateDlg::slotConfigureClicked()
433 {
434   Q_D(KEquityPriceUpdateDlg);
435   QPointer<EquityPriceUpdateConfDlg> dlg = new EquityPriceUpdateConfDlg(d->m_updatingPricePolicy);
436   if (dlg->exec() == QDialog::Accepted)
437     d->m_updatingPricePolicy = dlg->policy();
438   delete dlg;
439 }
440 
slotUpdateSelection()441 void KEquityPriceUpdateDlg::slotUpdateSelection()
442 {
443   Q_D(KEquityPriceUpdateDlg);
444   // Only enable the update button if there is a selection
445   d->ui->btnUpdateSelected->setEnabled(false);
446 
447   if (! d->ui->lvEquityList->selectedItems().empty())
448     d->ui->btnUpdateSelected->setEnabled(true);
449 }
450 
slotUpdateSelectedClicked()451 void KEquityPriceUpdateDlg::slotUpdateSelectedClicked()
452 {
453   Q_D(KEquityPriceUpdateDlg);
454   // disable sorting while the update is running to maintain the current order of items on which
455   // the update process depends and which could be changed with sorting enabled due to the updated values
456   d->ui->lvEquityList->setSortingEnabled(false);
457   auto item = d->ui->lvEquityList->invisibleRootItem()->child(0);
458   auto skipCnt = 1;
459   while (item && !item->isSelected()) {
460     item = d->ui->lvEquityList->invisibleRootItem()->child(skipCnt);
461     ++skipCnt;
462   }
463   d->m_webQuote.setDate(d->ui->m_fromDate->date(), d->ui->m_toDate->date());
464   if (item) {
465     d->ui->prgOnlineProgress->setMaximum(1 + d->ui->lvEquityList->invisibleRootItem()->childCount());
466     d->ui->prgOnlineProgress->setValue(skipCnt);
467     d->m_webQuote.launch(item->text(WEBID_COL), item->text(KMMID_COL), item->text(SOURCE_COL));
468 
469   } else {
470 
471     logErrorMessage("No security selected.");
472   }
473 }
474 
slotUpdateAllClicked()475 void KEquityPriceUpdateDlg::slotUpdateAllClicked()
476 {
477   Q_D(KEquityPriceUpdateDlg);
478   // disable sorting while the update is running to maintain the current order of items on which
479   // the update process depends and which could be changed with sorting enabled due to the updated values
480   d->ui->lvEquityList->setSortingEnabled(false);
481   QTreeWidgetItem* item = d->ui->lvEquityList->invisibleRootItem()->child(0);
482   if (item) {
483     d->ui->prgOnlineProgress->setMaximum(1 + d->ui->lvEquityList->invisibleRootItem()->childCount());
484     d->ui->prgOnlineProgress->setValue(1);
485     d->m_fUpdateAll = true;
486     d->m_webQuote.launch(item->text(WEBID_COL), item->text(KMMID_COL), item->text(SOURCE_COL));
487 
488   } else {
489     logErrorMessage("Security list is empty.");
490   }
491 }
492 
slotDateChanged()493 void KEquityPriceUpdateDlg::slotDateChanged()
494 {
495   Q_D(KEquityPriceUpdateDlg);
496   d->ui->m_fromDate->blockSignals(true);
497   d->ui->m_toDate->blockSignals(true);
498   if (d->ui->m_toDate->date() > QDate::currentDate())
499     d->ui->m_toDate->setDate(QDate::currentDate());
500   if (d->ui->m_fromDate->date() > d->ui->m_toDate->date())
501     d->ui->m_fromDate->setDate(d->ui->m_toDate->date());
502   d->ui->m_fromDate->blockSignals(false);
503   d->ui->m_toDate->blockSignals(false);
504 }
505 
slotQuoteFailed(const QString & _kmmID,const QString & _webID)506 void KEquityPriceUpdateDlg::slotQuoteFailed(const QString& _kmmID, const QString& _webID)
507 {
508   Q_D(KEquityPriceUpdateDlg);
509   auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL);
510   QTreeWidgetItem* item = nullptr;
511 
512   if (! foundItems.empty())
513     item = foundItems.at(0);
514 
515   // Give the user some options
516   int result;
517   if (_kmmID.contains(" ")) {
518     if (item)
519       result = KMessageBox::warningContinueCancel(this, i18n("Failed to retrieve an exchange rate for %1 from %2. It will be skipped this time.", _webID, item->text(SOURCE_COL)), i18n("Price Update Failed"));
520     else
521       return;
522   } else if (!item) {
523     return;
524   } else {
525     result = KMessageBox::questionYesNoCancel(this, QString::fromLatin1("<qt>%1</qt>").arg(i18n("Failed to retrieve a quote for %1 from %2.  Press <b>No</b> to remove the online price source from this security permanently, <b>Yes</b> to continue updating this security during future price updates or <b>Cancel</b> to stop the current update operation.", _webID, item->text(SOURCE_COL))), i18n("Price Update Failed"), KStandardGuiItem::yes(), KStandardGuiItem::no());
526   }
527 
528 
529   if (result == KMessageBox::No) {
530     // Disable price updates for this security
531 
532     MyMoneyFileTransaction ft;
533     try {
534       // Get this security (by ID)
535       MyMoneySecurity security = MyMoneyFile::instance()->security(_kmmID.toUtf8());
536 
537       // Set the quote source to blank
538       security.setValue("kmm-online-source", QString());
539       security.setValue("kmm-online-quote-system", QString());
540 
541       // Re-commit the security
542       MyMoneyFile::instance()->modifySecurity(security);
543       ft.commit();
544     } catch (const MyMoneyException &e) {
545       KMessageBox::error(this, QString("<qt>") + i18n("Cannot update security <b>%1</b>: %2", _webID, QString::fromLatin1(e.what())) + QString("</qt>"), i18n("Price Update Failed"));
546     }
547   }
548 
549   // As long as the user doesn't want to cancel, move on!
550   if (result != KMessageBox::Cancel) {
551     QTreeWidgetItem* next = nullptr;
552     d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1);
553     item->setSelected(false);
554 
555     // launch the NEXT one ... in case of m_fUpdateAll == false, we
556     // need to parse the list to find the next selected one
557     next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1);
558     if (!d->m_fUpdateAll) {
559       while (next && !next->isSelected()) {
560         d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1);
561         next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1);
562       }
563     }
564     if (next) {
565       d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL));
566     } else {
567       finishUpdate();
568     }
569   } else {
570     finishUpdate();
571   }
572 }
573 
slotReceivedCSVQuote(const QString & _kmmID,const QString & _webID,MyMoneyStatement & st)574 void KEquityPriceUpdateDlg::slotReceivedCSVQuote(const QString& _kmmID, const QString& _webID, MyMoneyStatement& st)
575 {
576   Q_D(KEquityPriceUpdateDlg);
577   auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL);
578   QTreeWidgetItem* item = nullptr;
579 
580   if (! foundItems.empty())
581     item = foundItems.at(0);
582 
583   QTreeWidgetItem* next = nullptr;
584 
585   if (item) {
586     auto file = MyMoneyFile::instance();
587     MyMoneySecurity fromCurrency, toCurrency;
588 
589     if (!_kmmID.contains(QLatin1Char(' '))) {
590       try {
591         toCurrency = MyMoneyFile::instance()->security(_kmmID);
592         fromCurrency = MyMoneyFile::instance()->security(toCurrency.tradingCurrency());
593       } catch (const MyMoneyException &) {
594         fromCurrency = toCurrency = MyMoneySecurity();
595       }
596 
597     } else {
598       QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive);
599       if (splitrx.indexIn(_kmmID) != -1) {
600         try {
601           fromCurrency = MyMoneyFile::instance()->security(splitrx.cap(2).toUtf8());
602           toCurrency = MyMoneyFile::instance()->security(splitrx.cap(1).toUtf8());
603         } catch (const MyMoneyException &) {
604           fromCurrency = toCurrency = MyMoneySecurity();
605         }
606       }
607     }
608 
609     if (d->m_updatingPricePolicy != eDialogs::UpdatePrice::All) {
610       QStringList qSources = WebPriceQuote::quoteSources();
611       for (auto it = st.m_listPrices.begin(); it != st.m_listPrices.end();) {
612         MyMoneyPrice storedPrice = file->price(toCurrency.id(), fromCurrency.id(), (*it).m_date, true);
613         bool priceValid = storedPrice.isValid();
614         if (!priceValid)
615           ++it;
616         else {
617           switch(d->m_updatingPricePolicy) {
618             case eDialogs::UpdatePrice::Missing:
619               it = st.m_listPrices.erase(it);
620               break;
621             case eDialogs::UpdatePrice::Downloaded:
622               if (!qSources.contains(storedPrice.source()))
623                 it = st.m_listPrices.erase(it);
624               else
625                 ++it;
626               break;
627             case eDialogs::UpdatePrice::SameSource:
628               if (storedPrice.source().compare((*it).m_sourceName) != 0)
629                 it = st.m_listPrices.erase(it);
630               else
631                 ++it;
632               break;
633             case eDialogs::UpdatePrice::Ask:
634             {
635               auto result = KMessageBox::questionYesNoCancel(this,
636                                                             i18n("For <b>%1</b> on <b>%2</b> price <b>%3</b> already exists.<br>"
637                                                                  "Do you want to replace it with <b>%4</b>?",
638                                                                  storedPrice.from(), storedPrice.date().toString(Qt::ISODate),
639                                                                  QString().setNum(storedPrice.rate(storedPrice.to()).toDouble(), 'g', 10),
640                                                                  QString().setNum((*it).m_amount.toDouble(), 'g', 10)),
641                                                             i18n("Price Already Exists"));
642               switch(result) {
643                 case KMessageBox::ButtonCode::Yes:
644                   ++it;
645                   break;
646                 case KMessageBox::ButtonCode::No:
647                   it = st.m_listPrices.erase(it);
648                   break;
649                 default:
650                 case KMessageBox::ButtonCode::Cancel:
651                   finishUpdate();
652                   return;
653                   break;
654               }
655               break;
656             }
657             default:
658               ++it;
659               break;
660           }
661         }
662       }
663     }
664 
665     if (!st.m_listPrices.isEmpty()) {
666       MyMoneyFileTransaction ft;
667       KMyMoneyUtils::processPriceList(st);
668       ft.commit();
669 
670       // latest price could be in the last or in the first row
671       MyMoneyStatement::Price priceClass;
672       if (st.m_listPrices.first().m_date > st.m_listPrices.last().m_date)
673         priceClass = st.m_listPrices.first();
674       else
675         priceClass = st.m_listPrices.last();
676 
677       // update latest price in dialog if applicable
678       auto latestDate = QDate::fromString(item->text(DATE_COL),Qt::ISODate);
679       if (latestDate <= priceClass.m_date && priceClass.m_amount.isPositive()) {
680         item->setText(PRICE_COL, priceClass.m_amount.formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision()));
681         item->setText(DATE_COL, priceClass.m_date.toString(Qt::ISODate));
682         item->setText(SOURCE_COL, priceClass.m_sourceName);
683       }
684       logStatusMessage(i18n("Price for %1 updated (id %2)", _webID, _kmmID));
685       // make sure to make OK button available
686     }
687     d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
688 
689     d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1);
690     item->setSelected(false);
691 
692     // launch the NEXT one ... in case of m_fUpdateAll == false, we
693     // need to parse the list to find the next selected one
694     next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1);
695     if (!d->m_fUpdateAll) {
696       while (next && !next->isSelected()) {
697         d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1);
698         next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1);
699       }
700     }
701   } else {
702     logErrorMessage(i18n("Received a price for %1 (id %2), but this symbol is not on the list. Aborting entire update.", _webID, _kmmID));
703   }
704 
705   if (next) {
706     d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL));
707   } else {
708     finishUpdate();
709   }
710 }
711 
slotReceivedQuote(const QString & _kmmID,const QString & _webID,const QDate & _date,const double & _price)712 void KEquityPriceUpdateDlg::slotReceivedQuote(const QString& _kmmID, const QString& _webID, const QDate& _date, const double& _price)
713 {
714   Q_D(KEquityPriceUpdateDlg);
715   auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL);
716   QTreeWidgetItem* item = nullptr;
717 
718   if (! foundItems.empty())
719     item = foundItems.at(0);
720 
721   QTreeWidgetItem* next = 0;
722 
723   if (item) {
724     if (_price > 0.0f && _date.isValid()) {
725       QDate date = _date;
726       if (date > QDate::currentDate())
727         date = QDate::currentDate();
728 
729       MyMoneyMoney price = MyMoneyMoney::ONE;
730       QString id = _kmmID.toUtf8();
731       MyMoneySecurity fromCurrency, toCurrency;
732       if (_kmmID.contains(" ") == 0) {
733         MyMoneySecurity security = MyMoneyFile::instance()->security(id);
734         QString factor = security.value("kmm-online-factor");
735         if (!factor.isEmpty()) {
736           price = price * MyMoneyMoney(factor);
737         }
738         try {
739           toCurrency = MyMoneyFile::instance()->security(id);
740           fromCurrency = MyMoneyFile::instance()->security(toCurrency.tradingCurrency());
741         } catch (const MyMoneyException &) {
742           fromCurrency = toCurrency = MyMoneySecurity();
743         }
744 
745       } else {
746         QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive);
747         if (splitrx.indexIn(_kmmID) != -1) {
748           try {
749             fromCurrency = MyMoneyFile::instance()->security(splitrx.cap(2).toUtf8());
750             toCurrency = MyMoneyFile::instance()->security(splitrx.cap(1).toUtf8());
751           } catch (const MyMoneyException &) {
752             fromCurrency = toCurrency = MyMoneySecurity();
753           }
754         }
755       }
756       price *= MyMoneyMoney(_price, MyMoneyMoney::precToDenom(toCurrency.pricePrecision()));
757 
758       item->setText(PRICE_COL, price.formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision()));
759       item->setText(DATE_COL, date.toString(Qt::ISODate));
760       logStatusMessage(i18n("Price for %1 updated (id %2)", _webID, _kmmID));
761       // make sure to make OK button available
762       d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
763     } else {
764       logErrorMessage(i18n("Received an invalid price for %1, unable to update.", _webID));
765     }
766 
767     d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1);
768     item->setSelected(false);
769 
770     // launch the NEXT one ... in case of m_fUpdateAll == false, we
771     // need to parse the list to find the next selected one
772     next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1);
773     if (!d->m_fUpdateAll) {
774       while (next && !next->isSelected()) {
775         d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1);
776         next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1);
777       }
778     }
779   } else {
780     logErrorMessage(i18n("Received a price for %1 (id %2), but this symbol is not on the list. Aborting entire update.", _webID, _kmmID));
781   }
782 
783   if (next) {
784     d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL));
785   } else {
786     finishUpdate();
787   }
788 }
789 
finishUpdate()790 void KEquityPriceUpdateDlg::finishUpdate()
791 {
792   Q_D(KEquityPriceUpdateDlg);
793   // we've run past the end, reset to the default value.
794   d->m_fUpdateAll = false;
795   // force progress bar to show 100%
796   d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->maximum());
797   // re-enable the sorting that was disabled during the update process
798   d->ui->lvEquityList->setSortingEnabled(true);
799 }
800 
801 // Make sure, that these definitions are only used within this file
802 // this does not seem to be necessary, but when building RPMs the
803 // build option 'final' is used and all CPP files are concatenated.
804 // So it could well be, that in another CPP file these definitions
805 // are also used.
806 #undef WEBID_COL
807 #undef NAME_COL
808 #undef PRICE_COL
809 #undef DATE_COL
810 #undef KMMID_COL
811 #undef SOURCE_COL
812