1 /***************************************************************************
2                           kmymoneyutils.cpp  -  description
3                              -------------------
4     begin                : Wed Feb 5 2003
5     copyright            : (C) 2000-2003 by Michael Edwardes
6     email                : mte@users.sourceforge.net
7                            Javier Campos Morales <javi_c@users.sourceforge.net>
8                            Felix Rodriguez <frodriguez@users.sourceforge.net>
9                            John C <thetacoturtle@users.sourceforge.net>
10                            Thomas Baumgart <ipwizard@users.sourceforge.net>
11                            Kevin Tambascio <ktambascio@users.sourceforge.net>
12                            (C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
13  ***************************************************************************/
14 
15 /***************************************************************************
16  *                                                                         *
17  *   This program is free software; you can redistribute it and/or modify  *
18  *   it under the terms of the GNU General Public License as published by  *
19  *   the Free Software Foundation; either version 2 of the License, or     *
20  *   (at your option) any later version.                                   *
21  *                                                                         *
22  ***************************************************************************/
23 
24 #include "kmymoneyutils.h"
25 
26 // ----------------------------------------------------------------------------
27 // QT Includes
28 
29 #include <QWidget>
30 #include <QApplication>
31 #include <QList>
32 #include <QPixmap>
33 #include <QWizard>
34 #include <QAbstractButton>
35 #include <QPixmapCache>
36 #include <QIcon>
37 #include <QPainter>
38 #include <QBitArray>
39 #include <QRegularExpression>
40 #include <QRegularExpressionMatch>
41 #include <QTemporaryFile>
42 #include <QFileInfo>
43 
44 // ----------------------------------------------------------------------------
45 // KDE Headers
46 
47 #include <KColorScheme>
48 #include <KLocalizedString>
49 #include <KGuiItem>
50 #include <KXmlGuiWindow>
51 #include <KMessageBox>
52 #include <KStandardGuiItem>
53 #include <KIO/StatJob>
54 #include <KIO/StoredTransferJob>
55 
56 // ----------------------------------------------------------------------------
57 // Project Includes
58 
59 #include "mymoneymoney.h"
60 #include "mymoneyexception.h"
61 #include "mymoneytransactionfilter.h"
62 #include "mymoneyfile.h"
63 #include "mymoneyaccount.h"
64 #include "mymoneysecurity.h"
65 #include "mymoneyschedule.h"
66 #include "mymoneypayee.h"
67 #include "mymoneytag.h"
68 #include "mymoneyprice.h"
69 #include "mymoneystatement.h"
70 #include "mymoneyforecast.h"
71 #include "mymoneysplit.h"
72 #include "mymoneytransaction.h"
73 #include "kmymoneysettings.h"
74 #include "icons.h"
75 #include "storageenums.h"
76 #include "mymoneyenums.h"
77 #include "kmymoneyplugin.h"
78 
79 using namespace Icons;
80 
KMyMoneyUtils()81 KMyMoneyUtils::KMyMoneyUtils()
82 {
83 }
84 
~KMyMoneyUtils()85 KMyMoneyUtils::~KMyMoneyUtils()
86 {
87 }
88 
occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence)89 const QString KMyMoneyUtils::occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence)
90 {
91   return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1());
92 }
93 
paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType)94 const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType)
95 {
96   return i18nc("Scheduled Transaction payment type", MyMoneySchedule::paymentMethodToString(paymentType).toLatin1());
97 }
98 
weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption)99 const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption)
100 {
101   return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1());
102 }
103 
scheduleTypeToString(eMyMoney::Schedule::Type type)104 const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type)
105 {
106   return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1());
107 }
108 
scheduleNewGuiItem()109 KGuiItem KMyMoneyUtils::scheduleNewGuiItem()
110 {
111   KGuiItem splitGuiItem(i18n("&New Schedule..."),
112                         Icons::get(Icon::DocumentNew),
113                         i18n("Create a new schedule."),
114                         i18n("Use this to create a new schedule."));
115 
116   return splitGuiItem;
117 }
118 
accountsFilterGuiItem()119 KGuiItem KMyMoneyUtils::accountsFilterGuiItem()
120 {
121   KGuiItem splitGuiItem(i18n("&Filter"),
122                         Icons::get(Icon::Filter),
123                         i18n("Filter out accounts"),
124                         i18n("Use this to filter out accounts"));
125 
126   return splitGuiItem;
127 }
128 
129 const char* homePageItems[] = {
130   I18N_NOOP("Payments"),
131   I18N_NOOP("Preferred accounts"),
132   I18N_NOOP("Payment accounts"),
133   I18N_NOOP("Favorite reports"),
134   I18N_NOOP("Forecast (schedule)"),
135   I18N_NOOP("Net worth forecast"),
136   I18N_NOOP("Forecast (history)"),        // unused, s.a. KSettingsHome::slotLoadItems()
137   I18N_NOOP("Assets and Liabilities"),
138   I18N_NOOP("Budget"),
139   I18N_NOOP("CashFlow"),
140   // insert new items above this comment
141   0
142 };
143 
homePageItemToString(const int idx)144 const QString KMyMoneyUtils::homePageItemToString(const int idx)
145 {
146   QString rc;
147   if (abs(idx) > 0 && abs(idx) < static_cast<int>(sizeof(homePageItems) / sizeof(homePageItems[0]))) {
148     rc = i18n(homePageItems[abs(idx-1)]);
149   }
150   return rc;
151 }
152 
stringToHomePageItem(const QString & txt)153 int KMyMoneyUtils::stringToHomePageItem(const QString& txt)
154 {
155   int idx = 0;
156   for (idx = 0; homePageItems[idx] != 0; ++idx) {
157     if (txt == i18n(homePageItems[idx]))
158       return idx + 1;
159   }
160   return 0;
161 }
162 
appendCorrectFileExt(QString & str,const QString & strExtToUse)163 bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse)
164 {
165   bool rc = false;
166 
167   if (!str.isEmpty()) {
168     //find last . deliminator
169     int nLoc = str.lastIndexOf('.');
170     if (nLoc != -1) {
171       QString strExt, strTemp;
172       strTemp = str.left(nLoc + 1);
173       strExt = str.right(str.length() - (nLoc + 1));
174       if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) {
175         // if the extension given contains a period, we remove ours
176         if (strExtToUse.indexOf('.') != -1)
177           strTemp = strTemp.left(strTemp.length() - 1);
178         //append extension to make complete file name
179         strTemp.append(strExtToUse);
180         str = strTemp;
181         rc = true;
182       }
183     } else {
184       str.append(QLatin1Char('.'));
185       str.append(strExtToUse);
186       rc = true;
187     }
188   }
189   return rc;
190 }
191 
checkConstants()192 void KMyMoneyUtils::checkConstants()
193 {
194   // TODO: port to kf5
195 #if 0
196   Q_ASSERT(static_cast<int>(KLocale::ParensAround) == static_cast<int>(MyMoneyMoney::ParensAround));
197   Q_ASSERT(static_cast<int>(KLocale::BeforeQuantityMoney) == static_cast<int>(MyMoneyMoney::BeforeQuantityMoney));
198   Q_ASSERT(static_cast<int>(KLocale::AfterQuantityMoney) == static_cast<int>(MyMoneyMoney::AfterQuantityMoney));
199   Q_ASSERT(static_cast<int>(KLocale::BeforeMoney) == static_cast<int>(MyMoneyMoney::BeforeMoney));
200   Q_ASSERT(static_cast<int>(KLocale::AfterMoney) == static_cast<int>(MyMoneyMoney::AfterMoney));
201 #endif
202 }
203 
variableCSS()204 QString KMyMoneyUtils::variableCSS()
205 {
206   QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color();
207   QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color();
208 
209   QString css;
210   css += "<style type=\"text/css\">\n<!--\n";
211   css += QString(".row-even, .item0 { background-color: %1; color: %2 }\n")
212          .arg(KMyMoneySettings::schemeColor(SchemeColor::ListBackground1).name()).arg(tcolor.name());
213   css += QString(".row-odd, .item1  { background-color: %1; color: %2 }\n")
214          .arg(KMyMoneySettings::schemeColor(SchemeColor::ListBackground2).name()).arg(tcolor.name());
215   css += QString("a { color: %1 }\n").arg(link.name());
216   css += "-->\n</style>\n";
217   return css;
218 }
219 
findResource(QStandardPaths::StandardLocation type,const QString & filename)220 QString KMyMoneyUtils::findResource(QStandardPaths::StandardLocation type, const QString& filename)
221 {
222   QLocale locale;
223   QString country;
224   QString localeName = locale.bcp47Name();
225   QString language = localeName;
226 
227   // extract language and country from the bcp47name
228   QRegularExpression regExp(QLatin1String("(\\w+)_(\\w+)"));
229   QRegularExpressionMatch match = regExp.match(localeName);
230   if (match.hasMatch()) {
231     language = match.captured(1);
232     country = match.captured(2);
233   }
234 
235   QString rc;
236 
237   // check that the placeholder is present and set things up
238   if (filename.indexOf("%1") != -1) {
239     /// @fixme somehow I have the impression, that language and country
240     ///    mappings to the filename are not correct. This certainly must
241     ///    be overhauled at some point in time (ipwizard, 2017-10-22)
242     QString mask = filename.arg("_%1.%2");
243     rc = QStandardPaths::locate(type, mask.arg(country, language));
244 
245     // search the given resource
246     if (rc.isEmpty()) {
247         mask = filename.arg("_%1");
248         rc = QStandardPaths::locate(type, mask.arg(language));
249     }
250     if (rc.isEmpty()) {
251         // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1());
252         rc = QStandardPaths::locate(type, mask.arg(country));
253     }
254     if (rc.isEmpty()) {
255         rc = QStandardPaths::locate(type, filename.arg(""));
256     }
257   } else {
258     rc = QStandardPaths::locate(type, filename);
259   }
260 
261   if (rc.isEmpty()) {
262     qWarning("No resource found for (%s,%s)", qPrintable(QStandardPaths::displayName(type)), qPrintable(filename));
263   }
264   return rc;
265 }
266 
stockSplit(const MyMoneyTransaction & t)267 const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t)
268 {
269   MyMoneySplit investmentAccountSplit;
270   foreach (const auto split, t.splits()) {
271     if (!split.accountId().isEmpty()) {
272       auto acc = MyMoneyFile::instance()->account(split.accountId());
273       if (acc.isInvest()) {
274         return split;
275       }
276       // if we have a reference to an investment account, we remember it here
277       if (acc.accountType() == eMyMoney::Account::Type::Investment)
278         investmentAccountSplit = split;
279     }
280   }
281   // if we haven't found a stock split, we see if we've seen
282   // an investment account on the way. If so, we return it.
283   if (!investmentAccountSplit.id().isEmpty())
284     return investmentAccountSplit;
285 
286   // if none was found, we return an empty split.
287   return MyMoneySplit();
288 }
289 
transactionType(const MyMoneyTransaction & t)290 KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t)
291 {
292   if (!stockSplit(t).id().isEmpty())
293     return InvestmentTransaction;
294 
295   if (t.splitCount() < 2) {
296     return Unknown;
297   } else if (t.splitCount() > 2) {
298     // FIXME check for loan transaction here
299     return SplitTransaction;
300   }
301   QString ida, idb;
302   const auto & splits = t.splits();
303   if (splits.size() > 0)
304     ida = splits[0].accountId();
305   if (splits.size() > 1)
306     idb = splits[1].accountId();
307   if (ida.isEmpty() || idb.isEmpty())
308     return Unknown;
309 
310   MyMoneyAccount a, b;
311   a = MyMoneyFile::instance()->account(ida);
312   b = MyMoneyFile::instance()->account(idb);
313   if ((a.accountGroup() == eMyMoney::Account::Type::Asset
314        || a.accountGroup() == eMyMoney::Account::Type::Liability)
315       && (b.accountGroup() == eMyMoney::Account::Type::Asset
316           || b.accountGroup() == eMyMoney::Account::Type::Liability))
317     return Transfer;
318   return Normal;
319 }
320 
calculateAutoLoan(const MyMoneySchedule & schedule,MyMoneyTransaction & transaction,const QMap<QString,MyMoneyMoney> & balances)321 void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap<QString, MyMoneyMoney>& balances)
322 {
323   try {
324     MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances);
325   } catch (const MyMoneyException &e) {
326     KMessageBox::detailedError(0, i18n("Unable to load schedule details"), QString::fromLatin1(e.what()));
327   }
328 }
329 
nextCheckNumber(const MyMoneyAccount & acc)330 QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc)
331 {
332   return getAdjacentNumber(acc.value("lastNumberUsed"), 1);
333 }
334 
nextFreeCheckNumber(const MyMoneyAccount & acc)335 QString KMyMoneyUtils::nextFreeCheckNumber(const MyMoneyAccount& acc)
336 {
337   auto file = MyMoneyFile::instance();
338   auto num = acc.value("lastNumberUsed");
339 
340   if (num.isEmpty())
341     num = QStringLiteral("1");
342 
343   // now check if this number has been used already
344   if (file->checkNoUsed(acc.id(), num)) {
345     // if a number has been entered which is immediately prior to
346     // an existing number, the next new number produced would clash
347     // so need to look ahead for free next number
348     // we limit that to a number of tries which depends on the
349     // number of splits in that account (we can't have more)
350     MyMoneyTransactionFilter filter(acc.id());
351     QList<MyMoneyTransaction> transactions;
352     file->transactionList(transactions, filter);
353     const int maxNumber = transactions.count();
354     for (int i = 0; i < maxNumber; i++) {
355       if (file->checkNoUsed(acc.id(), num)) {
356         //  increment and try again
357         num = getAdjacentNumber(num);
358       } else {
359         //  found a free number
360         break;
361       }
362     }
363   }
364   return num;
365 }
366 
getAdjacentNumber(const QString & number,int offset)367 QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset)
368 {
369   // make sure the offset is either -1 or 1
370   offset = (offset >= 0) ? 1 : -1;
371 
372   QString num = number;
373   //                   +-#1--+ +#2++-#3-++-#4--+
374   QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?"));
375   if (exp.indexIn(num) != -1) {
376     QString arg1 = exp.cap(1);
377     QString arg2 = exp.cap(2);
378     QString arg3 = QString::number(exp.cap(3).toULong() + offset);
379     QString arg4 = exp.cap(4);
380     num = QString("%1%2%3%4").arg(arg1, arg2, arg3, arg4);
381   } else {
382     num = QStringLiteral("1");
383   }
384   return num;
385 }
386 
numericPart(const QString & num)387 quint64 KMyMoneyUtils::numericPart(const QString & num)
388 {
389   quint64 num64 = 0;
390   QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?"));
391   if (exp.indexIn(num) != -1) {
392     // QString arg1 = exp.cap(1);
393     QString arg2 = exp.cap(2);
394     QString arg3 = QString::number(exp.cap(3).toULongLong());
395     // QString arg4 = exp.cap(4);
396     num64 = QString("%2%3").arg(arg2, arg3).toULongLong();
397   }
398   return num64;
399 }
400 
reconcileStateToString(eMyMoney::Split::State flag,bool text)401 QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text)
402 {
403   QString txt;
404   if (text) {
405     switch (flag) {
406       case eMyMoney::Split::State::NotReconciled:
407         txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled");
408         break;
409       case eMyMoney::Split::State::Cleared:
410         txt = i18nc("Reconciliation state 'Cleared'", "Cleared");
411         break;
412       case eMyMoney::Split::State::Reconciled:
413         txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled");
414         break;
415       case eMyMoney::Split::State::Frozen:
416         txt = i18nc("Reconciliation state 'Frozen'", "Frozen");
417         break;
418       default:
419         txt = i18nc("Unknown reconciliation state", "Unknown");
420         break;
421     }
422   } else {
423     switch (flag) {
424       case eMyMoney::Split::State::NotReconciled:
425         break;
426       case eMyMoney::Split::State::Cleared:
427         txt = i18nc("Reconciliation flag C", "C");
428         break;
429       case eMyMoney::Split::State::Reconciled:
430         txt = i18nc("Reconciliation flag R", "R");
431         break;
432       case eMyMoney::Split::State::Frozen:
433         txt = i18nc("Reconciliation flag F", "F");
434         break;
435       default:
436         txt = i18nc("Flag for unknown reconciliation state", "?");
437         break;
438     }
439   }
440   return txt;
441 }
442 
scheduledTransaction(const MyMoneySchedule & schedule)443 MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule)
444 {
445   MyMoneyTransaction t = schedule.transaction();
446 
447   try {
448     if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) {
449       calculateAutoLoan(schedule, t, QMap<QString, MyMoneyMoney>());
450     }
451   } catch (const MyMoneyException &e) {
452     qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), e.what());
453   }
454 
455   t.clearId();
456   t.setEntryDate(QDate());
457   return t;
458 }
459 
mainWindow()460 KXmlGuiWindow* KMyMoneyUtils::mainWindow()
461 {
462   foreach (QWidget *widget, QApplication::topLevelWidgets()) {
463     KXmlGuiWindow* result = dynamic_cast<KXmlGuiWindow*>(widget);
464     if (result)
465       return result;
466   }
467   return 0;
468 }
469 
updateWizardButtons(QWizard * wizard)470 void KMyMoneyUtils::updateWizardButtons(QWizard* wizard)
471 {
472   // setup text on buttons
473   wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next"));
474   wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text());
475 
476   // setup icons
477   wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon());
478   wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon());
479   wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon());
480   wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon());
481 }
482 
dissectTransaction(const MyMoneyTransaction & transaction,const MyMoneySplit & split,MyMoneySplit & assetAccountSplit,QList<MyMoneySplit> & feeSplits,QList<MyMoneySplit> & interestSplits,MyMoneySecurity & security,MyMoneySecurity & currency,eMyMoney::Split::InvestmentTransactionType & transactionType)483 void KMyMoneyUtils::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList<MyMoneySplit>& feeSplits, QList<MyMoneySplit>& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType)
484 {
485   // collect the splits. split references the stock account and should already
486   // be set up. assetAccountSplit references the corresponding asset account (maybe
487   // empty), feeSplits is the list of all expenses and interestSplits
488   // the list of all incomes
489   assetAccountSplit = MyMoneySplit(); // set to none to check later if it was assigned
490   auto file = MyMoneyFile::instance();
491   foreach (const auto tsplit, transaction.splits()) {
492     auto acc = file->account(tsplit.accountId());
493     if (tsplit.id() == split.id()) {
494       security = file->security(acc.currencyId());
495     } else if (acc.accountGroup() == eMyMoney::Account::Type::Expense) {
496       feeSplits.append(tsplit);
497       // feeAmount += tsplit.value();
498     } else if (acc.accountGroup() == eMyMoney::Account::Type::Income) {
499       interestSplits.append(tsplit);
500       // interestAmount += tsplit.value();
501     } else {
502       if (assetAccountSplit == MyMoneySplit()) // first asset Account should be our requested brokerage account
503         assetAccountSplit = tsplit;
504       else if (tsplit.value().isNegative())  // the rest (if present) is handled as fee or interest
505         feeSplits.append(tsplit);              // and shouldn't be allowed to override assetAccountSplit
506       else if (tsplit.value().isPositive())
507         interestSplits.append(tsplit);
508     }
509   }
510 
511   // determine transaction type
512   if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) {
513     transactionType = (!split.shares().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::AddShares : eMyMoney::Split::InvestmentTransactionType::RemoveShares;
514   } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) {
515     transactionType = (!split.value().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::BuyShares : eMyMoney::Split::InvestmentTransactionType::SellShares;
516   } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) {
517     transactionType = eMyMoney::Split::InvestmentTransactionType::Dividend;
518   } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) {
519     transactionType = eMyMoney::Split::InvestmentTransactionType::ReinvestDividend;
520   } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) {
521     transactionType = eMyMoney::Split::InvestmentTransactionType::Yield;
522   } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) {
523     transactionType = eMyMoney::Split::InvestmentTransactionType::SplitShares;
524   } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) {
525     transactionType = eMyMoney::Split::InvestmentTransactionType::InterestIncome;
526   } else
527     transactionType = eMyMoney::Split::InvestmentTransactionType::BuyShares;
528 
529   currency.setTradingSymbol("???");
530   try {
531     currency = file->security(transaction.commodity());
532   } catch (const MyMoneyException &) {
533   }
534 }
535 
processPriceList(const MyMoneyStatement & st)536 void KMyMoneyUtils::processPriceList(const MyMoneyStatement &st)
537 {
538   auto file = MyMoneyFile::instance();
539   QHash<QString, MyMoneySecurity> secBySymbol;
540   QHash<QString, MyMoneySecurity> secByName;
541 
542   const auto securityList = file->securityList();
543   for (const auto& sec : securityList) {
544     secBySymbol[sec.tradingSymbol()] = sec;
545     secByName[sec.name()] = sec;
546   }
547 
548   for (const auto& stPrice : st.m_listPrices) {
549     auto currency = file->baseCurrency().id();
550     QString security;
551 
552     if (!stPrice.m_strCurrency.isEmpty()) {
553       security = stPrice.m_strSecurity;
554       currency = stPrice.m_strCurrency;
555     } else if (secBySymbol.contains(stPrice.m_strSecurity)) {
556       security = secBySymbol[stPrice.m_strSecurity].id();
557       currency = file->security(file->security(security).tradingCurrency()).id();
558     } else if (secByName.contains(stPrice.m_strSecurity)) {
559       security = secByName[stPrice.m_strSecurity].id();
560       currency = file->security(file->security(security).tradingCurrency()).id();
561     } else
562       return;
563 
564     MyMoneyPrice price(security,
565                        currency,
566                        stPrice.m_date,
567                        stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName);
568     file->addPrice(price);
569   }
570 }
571 
deleteSecurity(const MyMoneySecurity & security,QWidget * parent)572 void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent)
573 {
574   QString msg, msg2;
575   QString dontAsk, dontAsk2;
576   if (security.isCurrency()) {
577     msg = i18n("<p>Do you really want to remove the currency <b>%1</b> from the file?</p>", security.name());
578     msg2 = i18n("<p>All exchange rates for currency <b>%1</b> will be lost.</p><p>Do you still want to continue?</p>", security.name());
579     dontAsk = "DeleteCurrency";
580     dontAsk2 = "DeleteCurrencyRates";
581   } else {
582     msg = i18n("<p>Do you really want to remove the %1 <b>%2</b> from the file?</p>", MyMoneySecurity::securityTypeToString(security.securityType()), security.name());
583     msg2 = i18n("<p>All price quotes for %1 <b>%2</b> will be lost.</p><p>Do you still want to continue?</p>", MyMoneySecurity::securityTypeToString(security.securityType()), security.name());
584     dontAsk = "DeleteSecurity";
585     dontAsk2 = "DeleteSecurityPrices";
586   }
587   if (KMessageBox::questionYesNo(parent, msg, i18n("Delete security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk) == KMessageBox::Yes) {
588     MyMoneyFileTransaction ft;
589     auto file = MyMoneyFile::instance();
590 
591     QBitArray skip((int)eStorage::Reference::Count);
592     skip.fill(true);
593     skip.clearBit((int)eStorage::Reference::Price);
594     if (file->isReferenced(security, skip)) {
595       if (KMessageBox::questionYesNo(parent, msg2, i18n("Delete prices"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk2) == KMessageBox::Yes) {
596         try {
597           QString secID = security.id();
598           foreach (auto priceEntry, file->priceList()) {
599             const MyMoneyPrice& price = priceEntry.first();
600             if (price.from() == secID || price.to() == secID)
601               file->removePrice(price);
602           }
603           ft.commit();
604           ft.restart();
605         } catch (const MyMoneyException &) {
606           qDebug("Cannot delete price");
607           return;
608         }
609       } else
610         return;
611     }
612     try {
613       if (security.isCurrency())
614         file->removeCurrency(security);
615       else
616         file->removeSecurity(security);
617       ft.commit();
618     } catch (const MyMoneyException &) {
619     }
620   }
621 }
622 
fileExists(const QUrl & url)623 bool KMyMoneyUtils::fileExists(const QUrl &url)
624 {
625     bool fileExists = false;
626     if (url.isValid()) {
627         if (url.isLocalFile() || url.scheme().isEmpty()) {
628           QFileInfo check_file(url.toLocalFile());
629           fileExists = check_file.exists() && check_file.isFile();
630 
631         } else {
632             short int detailLevel = 0; // Lowest level: file/dir/symlink/none
633             KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel);
634             bool noerror = statjob->exec();
635             if (noerror) {
636                 // We want a file
637                 fileExists = !statjob->statResult().isDir();
638             }
639             statjob->kill();
640         }
641     }
642     return fileExists;
643 }
644 
downloadFile(const QUrl & url)645 QString KMyMoneyUtils::downloadFile(const QUrl &url)
646 {
647   QString filename;
648   KIO::StoredTransferJob *transferjob = KIO::storedGet (url);
649 //  KJobWidgets::setWindow(transferjob, this);
650   if (! transferjob->exec()) {
651       KMessageBox::detailedError(nullptr,
652                                i18n("Error while loading file '%1'.", url.url()),
653                                transferjob->errorString(),
654                                i18n("File access error"));
655       return filename;
656   }
657 
658   QTemporaryFile file;
659   file.setAutoRemove(false);
660   file.open();
661   file.write(transferjob->data());
662   filename = file.fileName();
663   file.close();
664   return filename;
665 }
666 
newPayee(const QString & newnameBase,QString & id)667 bool KMyMoneyUtils::newPayee(const QString& newnameBase, QString& id)
668 {
669   bool doit = true;
670 
671   if (newnameBase != i18n("New Payee")) {
672     // Ask the user if that is what he intended to do?
673     const auto msg = i18n("<qt>Do you want to add <b>%1</b> as payer/receiver?</qt>", newnameBase);
674 
675     if (KMessageBox::questionYesNo(nullptr, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) {
676       doit = false;
677       // we should not keep the 'no' setting because that can confuse people like
678       // I have seen in some usability tests. So we just delete it right away.
679       KSharedConfigPtr kconfig = KSharedConfig::openConfig();
680       if (kconfig) {
681         kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee"));
682       }
683     }
684   }
685 
686   if (doit) {
687     MyMoneyFileTransaction ft;
688     try {
689       QString newname(newnameBase);
690       // adjust name until a unique name has been created
691       int count = 0;
692       for (;;) {
693         try {
694           MyMoneyFile::instance()->payeeByName(newname);
695           newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count);
696         } catch (const MyMoneyException &) {
697           break;
698         }
699       }
700 
701       MyMoneyPayee p;
702       p.setName(newname);
703       MyMoneyFile::instance()->addPayee(p);
704       id = p.id();
705       ft.commit();
706     } catch (const MyMoneyException &e) {
707       KMessageBox::detailedSorry(nullptr, i18n("Unable to add payee"), QString::fromLatin1(e.what()));
708       doit = false;
709     }
710   }
711   return doit;
712 }
713 
newTag(const QString & newnameBase,QString & id)714 void KMyMoneyUtils::newTag(const QString& newnameBase, QString& id)
715 {
716   bool doit = true;
717 
718   if (newnameBase != i18n("New Tag")) {
719     // Ask the user if that is what he intended to do?
720     const auto msg = i18n("<qt>Do you want to add <b>%1</b> as tag?</qt>", newnameBase);
721 
722     if (KMessageBox::questionYesNo(nullptr, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) {
723       doit = false;
724       // we should not keep the 'no' setting because that can confuse people like
725       // I have seen in some usability tests. So we just delete it right away.
726       KSharedConfigPtr kconfig = KSharedConfig::openConfig();
727       if (kconfig) {
728         kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag"));
729       }
730     }
731   }
732 
733   if (doit) {
734     MyMoneyFileTransaction ft;
735     try {
736       QString newname(newnameBase);
737       // adjust name until a unique name has been created
738       int count = 0;
739       for (;;) {
740         try {
741           MyMoneyFile::instance()->tagByName(newname);
742           newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count);
743         } catch (const MyMoneyException &) {
744           break;
745         }
746       }
747 
748       MyMoneyTag ta;
749       ta.setName(newname);
750       MyMoneyFile::instance()->addTag(ta);
751       id = ta.id();
752       ft.commit();
753     } catch (const MyMoneyException &e) {
754       KMessageBox::detailedSorry(nullptr, i18n("Unable to add tag"), QString::fromLatin1(e.what()));
755     }
756   }
757 }
758 
newInstitution(MyMoneyInstitution & institution)759 void KMyMoneyUtils::newInstitution(MyMoneyInstitution& institution)
760 {
761   auto file = MyMoneyFile::instance();
762 
763   MyMoneyFileTransaction ft;
764 
765   try {
766     file->addInstitution(institution);
767     ft.commit();
768 
769   } catch (const MyMoneyException &e) {
770     KMessageBox::information(nullptr, i18n("Cannot add institution: %1", QString::fromLatin1(e.what())));
771   }
772 }
773 
debug()774 QDebug KMyMoneyUtils::debug()
775 {
776   return qDebug() << QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz"));
777 }
778 
forecast()779 MyMoneyForecast KMyMoneyUtils::forecast()
780 {
781   MyMoneyForecast forecast;
782 
783   // override object defaults with those of the application
784   forecast.setForecastCycles(KMyMoneySettings::forecastCycles());
785   forecast.setAccountsCycle(KMyMoneySettings::forecastAccountCycle());
786   forecast.setHistoryStartDate(QDate::currentDate().addDays(-forecast.forecastCycles()*forecast.accountsCycle()));
787   forecast.setHistoryEndDate(QDate::currentDate().addDays(-1));
788   forecast.setForecastDays(KMyMoneySettings::forecastDays());
789   forecast.setBeginForecastDay(KMyMoneySettings::beginForecastDay());
790   forecast.setForecastMethod(KMyMoneySettings::forecastMethod());
791   forecast.setHistoryMethod(KMyMoneySettings::historyMethod());
792   forecast.setIncludeFutureTransactions(KMyMoneySettings::includeFutureTransactions());
793   forecast.setIncludeScheduledTransactions(KMyMoneySettings::includeScheduledTransactions());
794 
795   return forecast;
796 }
797 
canUpdateAllAccounts()798 bool KMyMoneyUtils::canUpdateAllAccounts()
799 {
800   const auto file = MyMoneyFile::instance();
801   auto rc = false;
802   if (!file->storageAttached())
803     return rc;
804 
805   QList<MyMoneyAccount> accList;
806   file->accountList(accList);
807   QList<MyMoneyAccount>::const_iterator it_a;
808   auto it_p = pPlugins.online.constEnd();
809   for (it_a = accList.constBegin(); (it_p == pPlugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) {
810     if ((*it_a).hasOnlineMapping()) {
811       // check if provider is available
812       it_p = pPlugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower());
813       if (it_p != pPlugins.online.constEnd()) {
814         QStringList protocols;
815         (*it_p)->protocols(protocols);
816         if (!protocols.isEmpty()) {
817           rc = true;
818           break;
819         }
820       }
821     }
822   }
823   return rc;
824 }
825 
showStatementImportResult(const QStringList & resultMessages,uint statementCount)826 void KMyMoneyUtils::showStatementImportResult(const QStringList& resultMessages, uint statementCount)
827 {
828   KMessageBox::informationList(nullptr,
829                                 i18np("One statement has been processed with the following results:",
830                                       "%1 statements have been processed with the following results:",
831                                       statementCount),
832                                 !resultMessages.isEmpty() ?
833                                     resultMessages :
834                                     QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) },
835                                 i18n("Statement import statistics"));
836 }
837 
normalizeNumericString(const qreal & val,const QLocale & loc,const char f,const int prec)838 QString KMyMoneyUtils::normalizeNumericString(const qreal& val, const QLocale& loc, const char f, const int prec)
839 {
840     return loc.toString(val, f, prec)
841             .remove(loc.groupSeparator())
842             .remove(QRegularExpression("0+$"))
843             .remove(QRegularExpression("\\" + loc.decimalPoint() + "$"));
844 }
845