1 /*
2  * Copyright 2004-2018  Thomas Baumgart <tbaumgart@kde.org>
3  * Copyright 2017       Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
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 "kcurrencycalculator.h"
20 
21 // ----------------------------------------------------------------------------
22 // QT Includes
23 
24 #include <QButtonGroup>
25 #include <QLabel>
26 #include <QRadioButton>
27 #include <QCheckBox>
28 #include <QPushButton>
29 #include <QDialogButtonBox>
30 #include <QPointer>
31 
32 // ----------------------------------------------------------------------------
33 // KDE Includes
34 
35 #include <KLocalizedString>
36 
37 // ----------------------------------------------------------------------------
38 // Project Includes
39 
40 #include "ui_kcurrencycalculator.h"
41 
42 #include "mymoneyfile.h"
43 #include "mymoneyaccount.h"
44 #include "mymoneysecurity.h"
45 #include "mymoneyprice.h"
46 #include "mymoneymoney.h"
47 #include "mymoneyexception.h"
48 #include "mymoneysplit.h"
49 #include "mymoneytransaction.h"
50 #include "kmymoneysettings.h"
51 
52 class KCurrencyCalculatorPrivate
53 {
54   Q_DISABLE_COPY(KCurrencyCalculatorPrivate)
55   Q_DECLARE_PUBLIC(KCurrencyCalculator)
56 
57 public:
KCurrencyCalculatorPrivate(KCurrencyCalculator * qq,const MyMoneySecurity & from,const MyMoneySecurity & to,const MyMoneyMoney & value,const MyMoneyMoney & shares,const QDate & date,const signed64 resultFraction)58   explicit KCurrencyCalculatorPrivate(KCurrencyCalculator *qq,
59                                       const MyMoneySecurity& from,
60                                       const MyMoneySecurity& to,
61                                       const MyMoneyMoney& value,
62                                       const MyMoneyMoney& shares,
63                                       const QDate& date,
64                                       const signed64 resultFraction) :
65     q_ptr(qq),
66     ui(new Ui::KCurrencyCalculator),
67     m_fromCurrency(from),
68     m_toCurrency(to),
69     m_result(shares.abs()),
70     m_value(value.abs()),
71     m_date(date),
72     m_resultFraction(resultFraction)
73   {
74   }
75 
~KCurrencyCalculatorPrivate()76   ~KCurrencyCalculatorPrivate()
77   {
78     delete ui;
79   }
80 
init()81   void init()
82   {
83     Q_Q(KCurrencyCalculator);
84     ui->setupUi(q);
85     auto file = MyMoneyFile::instance();
86 
87     //set main widget of QDialog
88     ui->buttonGroup1->setId(ui->m_amountButton, 0);
89     ui->buttonGroup1->setId(ui->m_rateButton, 1);
90 
91     ui->m_dateFrame->hide();
92     if (m_date.isValid())
93       ui->m_dateEdit->setDate(m_date);
94     else
95       ui->m_dateEdit->setDate(QDate::currentDate());
96 
97     ui->m_fromCurrencyText->setText(QString(MyMoneySecurity::securityTypeToString(m_fromCurrency.securityType()) + ' ' + (m_fromCurrency.isCurrency() ? m_fromCurrency.id() : m_fromCurrency.tradingSymbol())));
98     ui->m_toCurrencyText->setText(QString(MyMoneySecurity::securityTypeToString(m_toCurrency.securityType()) + ' ' + (m_toCurrency.isCurrency() ? m_toCurrency.id() : m_toCurrency.tradingSymbol())));
99 
100     //set bold font
101     auto boldFont = ui->m_fromCurrencyText->font();
102     boldFont.setBold(true);
103     ui->m_fromCurrencyText->setFont(boldFont);
104     boldFont = ui->m_toCurrencyText->font();
105     boldFont.setBold(true);
106     ui->m_toCurrencyText->setFont(boldFont);
107 
108     ui->m_fromAmount->setText(m_value.formatMoney(QString(), MyMoneyMoney::denomToPrec(m_fromCurrency.smallestAccountFraction())));
109 
110     ui->m_dateText->setText(QLocale().toString(m_date));
111 
112     ui->m_updateButton->setChecked(KMyMoneySettings::priceHistoryUpdate());
113 
114     // setup initial result
115     if (m_result == MyMoneyMoney() && !m_value.isZero()) {
116       const MyMoneyPrice &pr = file->price(m_fromCurrency.id(), m_toCurrency.id(), m_date);
117       if (pr.isValid()) {
118         m_result = m_value * pr.rate(m_toCurrency.id());
119       }
120     }
121 
122     // fill in initial values
123     ui->m_toAmount->setPrecision(MyMoneyMoney::denomToPrec(m_resultFraction));
124     ui->m_toAmount->setValue(m_result);
125 
126     ui->m_conversionRate->setPrecision(m_fromCurrency.pricePrecision());
127 
128     q->connect(ui->m_amountButton, &QAbstractButton::clicked, q, &KCurrencyCalculator::slotSetToAmount);
129     q->connect(ui->m_rateButton, &QAbstractButton::clicked, q, &KCurrencyCalculator::slotSetExchangeRate);
130 
131     q->connect(ui->m_toAmount, &AmountEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateResult);
132     q->connect(ui->m_conversionRate, &AmountEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateRate);
133 
134     // use this as the default
135     ui->m_amountButton->animateClick();
136     q->slotUpdateResult(ui->m_toAmount->text());
137 
138     // If the from security is not a currency, we only allow entering a price
139     if (!m_fromCurrency.isCurrency()) {
140       ui->m_rateButton->animateClick();
141       ui->m_amountButton->hide();
142       ui->m_toAmount->hide();
143     }
144   }
145 
updateExample(const MyMoneyMoney & price)146   void updateExample(const MyMoneyMoney& price)
147   {
148     QString msg;
149     if (price.isZero()) {
150       msg = QString("1 %1 = ? %2").arg(m_fromCurrency.tradingSymbol())
151             .arg(m_toCurrency.tradingSymbol());
152       if (m_fromCurrency.isCurrency()) {
153         msg += QString("\n");
154         msg += QString("1 %1 = ? %2").arg(m_toCurrency.tradingSymbol())
155                .arg(m_fromCurrency.tradingSymbol());
156       }
157     } else {
158       msg = QString("1 %1 = %2 %3").arg(m_fromCurrency.tradingSymbol())
159             .arg(price.formatMoney(QString(), m_fromCurrency.pricePrecision()))
160             .arg(m_toCurrency.tradingSymbol());
161       if (m_fromCurrency.isCurrency()) {
162         msg += QString("\n");
163         msg += QString("1 %1 = %2 %3").arg(m_toCurrency.tradingSymbol())
164                .arg((MyMoneyMoney::ONE / price).formatMoney(QString(), m_toCurrency.pricePrecision()))
165                .arg(m_fromCurrency.tradingSymbol());
166       }
167     }
168     ui->m_conversionExample->setText(msg);
169     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!price.isZero());
170   }
171 
172   KCurrencyCalculator     *q_ptr;
173   Ui::KCurrencyCalculator *ui;
174   MyMoneySecurity          m_fromCurrency;
175   MyMoneySecurity          m_toCurrency;
176   MyMoneyMoney             m_result;
177   MyMoneyMoney             m_value;
178   QDate                    m_date;
179   signed64                 m_resultFraction;
180 };
181 
KCurrencyCalculator(const MyMoneySecurity & from,const MyMoneySecurity & to,const MyMoneyMoney & value,const MyMoneyMoney & shares,const QDate & date,const signed64 resultFraction,QWidget * parent)182 KCurrencyCalculator::KCurrencyCalculator(const MyMoneySecurity& from,
183                                          const MyMoneySecurity& to,
184                                          const MyMoneyMoney& value,
185                                          const MyMoneyMoney& shares,
186                                          const QDate& date,
187                                          const signed64 resultFraction,
188                                          QWidget *parent) :
189   QDialog(parent),
190   d_ptr(new KCurrencyCalculatorPrivate(this,
191                                        from,
192                                        to,
193                                        value,
194                                        shares,
195                                        date,
196                                        resultFraction))
197 {
198   Q_D(KCurrencyCalculator);
199   d->init();
200 }
201 
~KCurrencyCalculator()202 KCurrencyCalculator::~KCurrencyCalculator()
203 {
204   Q_D(KCurrencyCalculator);
205   delete d;
206 }
207 
setupSplitPrice(MyMoneyMoney & shares,const MyMoneyTransaction & t,const MyMoneySplit & s,const QMap<QString,MyMoneyMoney> & priceInfo,QWidget * parentWidget)208 bool KCurrencyCalculator::setupSplitPrice(MyMoneyMoney& shares,
209                                           const MyMoneyTransaction& t,
210                                           const MyMoneySplit& s,
211                                           const QMap<QString,
212                                           MyMoneyMoney>& priceInfo,
213                                           QWidget* parentWidget)
214 {
215   auto rc = true;
216   auto file = MyMoneyFile::instance();
217 
218   if (!s.value().isZero()) {
219     auto cat = file->account(s.accountId());
220     MyMoneySecurity toCurrency;
221     toCurrency = file->security(cat.currencyId());
222     // determine the fraction required for this category/account
223     int fract = cat.fraction(toCurrency);
224 
225     if (cat.currencyId() != t.commodity()) {
226 
227       MyMoneyMoney toValue;
228       auto fromCurrency = file->security(t.commodity());
229       // display only positive values to the user
230       auto fromValue = s.value().abs();
231 
232       // if we had a price info in the beginning, we use it here
233       if (priceInfo.find(cat.currencyId()) != priceInfo.end()) {
234         toValue = (fromValue * priceInfo[cat.currencyId()]).convert(fract);
235       }
236       // if the shares are still 0, we need to change that
237       if (toValue.isZero()) {
238         const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), t.postDate());
239         // if the price is valid calculate the shares. If it is invalid
240         // assume a conversion rate of 1.0
241         if (price.isValid()) {
242           toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract);
243         } else {
244           toValue = fromValue;
245         }
246       }
247 
248       // now present all that to the user
249       QPointer<KCurrencyCalculator> calc =
250         new KCurrencyCalculator(fromCurrency,
251                                 toCurrency,
252                                 fromValue,
253                                 toValue,
254                                 t.postDate(),
255                                 fract,
256                                 parentWidget);
257 
258       if (calc->exec() == QDialog::Rejected) {
259         rc = false;
260       } else
261         shares = (s.value() * calc->price()).convert(fract);
262 
263       delete calc;
264 
265     } else {
266       shares = s.value().convert(fract);
267     }
268   } else
269     shares = s.value();
270 
271   return rc;
272 }
273 
setupPriceEditor()274 void KCurrencyCalculator::setupPriceEditor()
275 {
276   Q_D(KCurrencyCalculator);
277   d->ui->m_dateFrame->show();
278   d->ui->m_amountDateFrame->hide();
279   d->ui->m_updateButton->setChecked(true);
280   d->ui->m_updateButton->hide();
281 }
282 
slotSetToAmount()283 void KCurrencyCalculator::slotSetToAmount()
284 {
285   Q_D(KCurrencyCalculator);
286   d->ui->m_rateButton->setChecked(false);
287   d->ui->m_toAmount->setEnabled(true);
288   d->ui->m_conversionRate->setEnabled(false);
289 }
290 
slotSetExchangeRate()291 void KCurrencyCalculator::slotSetExchangeRate()
292 {
293   Q_D(KCurrencyCalculator);
294   d->ui->m_amountButton->setChecked(false);
295   d->ui->m_toAmount->setEnabled(false);
296   d->ui->m_conversionRate->setEnabled(true);
297 }
298 
slotUpdateResult(const QString &)299 void KCurrencyCalculator::slotUpdateResult(const QString& /*txt*/)
300 {
301   Q_D(KCurrencyCalculator);
302   MyMoneyMoney result = d->ui->m_toAmount->value();
303   MyMoneyMoney price(0, 1);
304 
305   if (result.isNegative()) {
306     d->ui->m_toAmount->setValue(-result);
307     slotUpdateResult(QString());
308     return;
309   }
310 
311   if (!result.isZero()) {
312     price = result / d->m_value;
313 
314     d->ui->m_conversionRate->setValue(price);
315     d->m_result = (d->m_value * price).convert(d->m_resultFraction);
316     d->ui->m_toAmount->setValue(d->m_result);
317   }
318   d->updateExample(price);
319 }
320 
slotUpdateRate(const QString &)321 void KCurrencyCalculator::slotUpdateRate(const QString& /*txt*/)
322 {
323   Q_D(KCurrencyCalculator);
324   auto price = d->ui->m_conversionRate->value();
325 
326   if (price.isNegative()) {
327     d->ui->m_conversionRate->setValue(-price);
328     slotUpdateRate(QString());
329     return;
330   }
331 
332   if (!price.isZero()) {
333     d->ui->m_conversionRate->setValue(price);
334     d->m_result = (d->m_value * price).convert(d->m_resultFraction);
335     d->ui->m_toAmount->setValue(d->m_result);
336   }
337   d->updateExample(price);
338 }
339 
accept()340 void KCurrencyCalculator::accept()
341 {
342   Q_D(KCurrencyCalculator);
343   if (d->ui->m_conversionRate->isEnabled())
344     slotUpdateRate(QString());
345   else
346     slotUpdateResult(QString());
347 
348   if (d->ui->m_updateButton->isChecked()) {
349     auto pr = MyMoneyFile::instance()->price(d->m_fromCurrency.id(), d->m_toCurrency.id(), d->ui->m_dateEdit->date());
350     if (!pr.isValid()
351         || pr.date() != d->ui->m_dateEdit->date()
352         || (pr.date() == d->ui->m_dateEdit->date() && pr.rate(d->m_fromCurrency.id()) != price())) {
353       pr = MyMoneyPrice(d->m_fromCurrency.id(), d->m_toCurrency.id(), d->ui->m_dateEdit->date(), price(), i18n("User"));
354       MyMoneyFileTransaction ft;
355       try {
356         MyMoneyFile::instance()->addPrice(pr);
357         ft.commit();
358       } catch (const MyMoneyException &) {
359         qDebug("Cannot add price");
360       }
361     }
362   }
363 
364   // remember setting for next round
365   KMyMoneySettings::setPriceHistoryUpdate(d->ui->m_updateButton->isChecked());
366   QDialog::accept();
367 }
368 
price() const369 MyMoneyMoney KCurrencyCalculator::price() const
370 {
371   Q_D(const KCurrencyCalculator);
372   // This should fix https://bugs.kde.org/show_bug.cgi?id=205254 and
373   // https://bugs.kde.org/show_bug.cgi?id=325953 as well as
374   // https://bugs.kde.org/show_bug.cgi?id=300965
375   if (d->ui->m_amountButton->isChecked())
376     return d->ui->m_toAmount->value().abs() / d->m_value.abs();
377   else
378     return d->ui->m_conversionRate->value();
379 }
380