1 /*
2 * Copyright 2000-2003 Michael Edwardes <mte@users.sourceforge.net>
3 * Copyright 2001-2002 Felix Rodriguez <frodriguez@users.sourceforge.net>
4 * Copyright 2002-2004 Kevin Tambascio <ktambascio@users.sourceforge.net>
5 * Copyright 2004-2005 Ace Jones <acejones@users.sourceforge.net>
6 * Copyright 2006-2019 Thomas Baumgart <tbaumgart@kde.org>
7 * Copyright 2006 Darren Gould <darren_gould@gmx.de>
8 * Copyright 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
9 *
10 * This program is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU General Public License as
12 * published by the Free Software Foundation; either version 2 of
13 * the License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24 #include "mymoneyfile.h"
25
26 #include <utility>
27
28 // ----------------------------------------------------------------------------
29 // QT Includes
30
31 #include <QString>
32 #include <QList>
33 #include <QUuid>
34 #include <QLocale>
35 #include <QBitArray>
36 #include <QDebug>
37
38 // ----------------------------------------------------------------------------
39 // KDE Includes
40
41 #include <KLocalizedString>
42
43 // ----------------------------------------------------------------------------
44 // Project Includes
45
46 #include "mymoneystoragemgr.h"
47 #include "mymoneyinstitution.h"
48 #include "mymoneyaccount.h"
49 #include "mymoneyaccountloan.h"
50 #include "mymoneysecurity.h"
51 #include "mymoneyreport.h"
52 #include "mymoneybalancecache.h"
53 #include "mymoneybudget.h"
54 #include "mymoneyprice.h"
55 #include "mymoneypayee.h"
56 #include "mymoneytag.h"
57 #include "mymoneyschedule.h"
58 #include "mymoneysplit.h"
59 #include "mymoneytransaction.h"
60 #include "mymoneycostcenter.h"
61 #include "mymoneyexception.h"
62 #include "onlinejob.h"
63 #include "storageenums.h"
64 #include "mymoneyenums.h"
65
66 // include the following line to get a 'cout' for debug purposes
67 // #include <iostream>
68
69 using namespace eMyMoney;
70
71 const QString MyMoneyFile::AccountSeparator = QChar(':');
72
73 MyMoneyFile MyMoneyFile::file;
74
75 typedef QList<std::pair<QString, QDate> > BalanceNotifyList;
76 typedef QMap<QString, bool> CacheNotifyList;
77
78 /// @todo make this template based
79 class MyMoneyNotification
80 {
81 public:
MyMoneyNotification(File::Mode mode,const MyMoneyTransaction & t)82 MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) :
83 m_objType(File::Object::Transaction),
84 m_notificationMode(mode),
85 m_id(t.id()) {
86 }
87
MyMoneyNotification(File::Mode mode,const MyMoneyAccount & acc)88 MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) :
89 m_objType(File::Object::Account),
90 m_notificationMode(mode),
91 m_id(acc.id()) {
92 }
93
MyMoneyNotification(File::Mode mode,const MyMoneyInstitution & institution)94 MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) :
95 m_objType(File::Object::Institution),
96 m_notificationMode(mode),
97 m_id(institution.id()) {
98 }
99
MyMoneyNotification(File::Mode mode,const MyMoneyPayee & payee)100 MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) :
101 m_objType(File::Object::Payee),
102 m_notificationMode(mode),
103 m_id(payee.id()) {
104 }
105
MyMoneyNotification(File::Mode mode,const MyMoneyTag & tag)106 MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) :
107 m_objType(File::Object::Tag),
108 m_notificationMode(mode),
109 m_id(tag.id()) {
110 }
111
MyMoneyNotification(File::Mode mode,const MyMoneySchedule & schedule)112 MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) :
113 m_objType(File::Object::Schedule),
114 m_notificationMode(mode),
115 m_id(schedule.id()) {
116 }
117
MyMoneyNotification(File::Mode mode,const MyMoneySecurity & security)118 MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) :
119 m_objType(File::Object::Security),
120 m_notificationMode(mode),
121 m_id(security.id()) {
122 }
123
MyMoneyNotification(File::Mode mode,const onlineJob & job)124 MyMoneyNotification(File::Mode mode, const onlineJob& job) :
125 m_objType(File::Object::OnlineJob),
126 m_notificationMode(mode),
127 m_id(job.id()) {
128 }
129
objectType() const130 File::Object objectType() const {
131 return m_objType;
132 }
notificationMode() const133 File::Mode notificationMode() const {
134 return m_notificationMode;
135 }
id() const136 const QString& id() const {
137 return m_id;
138 }
139
140 protected:
MyMoneyNotification(File::Object obj,File::Mode mode,const QString & id)141 MyMoneyNotification(File::Object obj,
142 File::Mode mode,
143 const QString& id) :
144 m_objType(obj),
145 m_notificationMode(mode),
146 m_id(id) {}
147
148 private:
149 File::Object m_objType;
150 File::Mode m_notificationMode;
151 QString m_id;
152 };
153
154
155
156
157
158 class MyMoneyFile::Private
159 {
160 public:
Private()161 Private() :
162 m_storage(0),
163 m_inTransaction(false) {}
164
~Private()165 ~Private() {
166 delete m_storage;
167 }
168 /**
169 * This method is used to add an id to the list of objects
170 * to be removed from the cache. If id is empty, then nothing is added to the list.
171 *
172 * @param id id of object to be notified
173 * @param reload reload the object (@c true) or not (@c false). The default is @c true
174 * @see attach, detach
175 */
addCacheNotification(const QString & id,const QDate & date)176 void addCacheNotification(const QString& id, const QDate& date) {
177 if (!id.isEmpty())
178 m_balanceNotifyList.append(std::make_pair(id, date));
179 }
180
181 /**
182 * This method is used to clear the notification list
183 */
clearCacheNotification()184 void clearCacheNotification() {
185 // reset list to be empty
186 m_balanceNotifyList.clear();
187 }
188
189 /**
190 * This method is used to clear all
191 * objects mentioned in m_notificationList from the cache.
192 */
notify()193 void notify() {
194 foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) {
195 m_balanceChangedSet += i.first;
196 if (i.second.isValid()) {
197 m_balanceCache.clear(i.first, i.second);
198 } else {
199 m_balanceCache.clear(i.first);
200 }
201 }
202
203 clearCacheNotification();
204 }
205
206 /**
207 * This method checks if a storage object is attached and
208 * throws and exception if not.
209 */
checkStorage() const210 inline void checkStorage() const {
211 if (m_storage == 0)
212 throw MYMONEYEXCEPTION_CSTRING("No storage object attached to MyMoneyFile");
213 }
214
215 /**
216 * This method checks that a transaction has been started with
217 * startTransaction() and throws an exception otherwise. Calls
218 * checkStorage() to make sure a storage object is present and attached.
219 */
checkTransaction(const char * txt) const220 void checkTransaction(const char* txt) const {
221 checkStorage();
222 if (!m_inTransaction)
223 throw MYMONEYEXCEPTION(QString::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(txt)));
224 }
225
priceChanged(const MyMoneyFile & file,const MyMoneyPrice price)226 void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) {
227 // get all affected accounts and add them to the m_valueChangedSet
228 QList<MyMoneyAccount> accList;
229 file.accountList(accList);
230 QList<MyMoneyAccount>::const_iterator account_it;
231 for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) {
232 QString currencyId = account_it->currencyId();
233 if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) {
234 // this account is not in the base currency and the price affects it's value
235 m_valueChangedSet.insert(account_it->id());
236 }
237 }
238 }
239
240 /**
241 * This member points to the storage strategy
242 */
243 MyMoneyStorageMgr *m_storage;
244
245
246 bool m_inTransaction;
247 MyMoneySecurity m_baseCurrency;
248
249 /**
250 * @brief Cache for MyMoneyObjects
251 *
252 * It is also used to emit the objectAdded() and objectModified() signals.
253 * => If one of these signals is used, you must use this cache.
254 */
255 MyMoneyPriceList m_priceCache;
256 MyMoneyBalanceCache m_balanceCache;
257
258 /**
259 * This member keeps a list of account ids to notify
260 * after a single operation is completed. The balance cache
261 * is cleared for that account and all dates on or after
262 * the one supplied. If the date is invalid, the entire
263 * balance cache is cleared for that account.
264 */
265 BalanceNotifyList m_balanceNotifyList;
266
267 /**
268 * This member keeps a list of account ids for which
269 * a balanceChanged() signal needs to be emitted when
270 * a set of operations has been committed.
271 *
272 * @sa MyMoneyFile::commitTransaction()
273 */
274 QSet<QString> m_balanceChangedSet;
275
276 /**
277 * This member keeps a list of account ids for which
278 * a valueChanged() signal needs to be emitted when
279 * a set of operations has been committed.
280 *
281 * @sa MyMoneyFile::commitTransaction()
282 */
283 QSet<QString> m_valueChangedSet;
284
285 /**
286 * This member keeps the list of changes in the engine
287 * in historical order. The type can be 'added', 'modified'
288 * or removed.
289 */
290 QList<MyMoneyNotification> m_changeSet;
291 };
292
293
294 class MyMoneyNotifier
295 {
296 public:
MyMoneyNotifier(MyMoneyFile::Private * file)297 MyMoneyNotifier(MyMoneyFile::Private* file) {
298 m_file = file; m_file->clearCacheNotification();
299 }
~MyMoneyNotifier()300 ~MyMoneyNotifier() {
301 m_file->notify();
302 }
303 private:
304 MyMoneyFile::Private* m_file;
305 };
306
307
308
MyMoneyFile()309 MyMoneyFile::MyMoneyFile() :
310 d(new Private)
311 {
312 }
313
~MyMoneyFile()314 MyMoneyFile::~MyMoneyFile()
315 {
316 delete d;
317 }
318
MyMoneyFile(MyMoneyStorageMgr * storage)319 MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) :
320 d(new Private)
321 {
322 attachStorage(storage);
323 }
324
instance()325 MyMoneyFile* MyMoneyFile::instance()
326 {
327 return &file;
328 }
329
attachStorage(MyMoneyStorageMgr * const storage)330 void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage)
331 {
332 if (d->m_storage != 0)
333 throw MYMONEYEXCEPTION_CSTRING("Storage already attached");
334
335 if (storage == 0)
336 throw MYMONEYEXCEPTION_CSTRING("Storage must not be 0");
337
338 d->m_storage = storage;
339
340 // force reload of base currency
341 d->m_baseCurrency = MyMoneySecurity();
342
343 // and the whole cache
344 d->m_balanceCache.clear();
345 d->m_priceCache.clear();
346
347 // notify application about new data availability
348 emit beginChangeNotification();
349 emit dataChanged();
350 emit endChangeNotification();
351 }
352
detachStorage(MyMoneyStorageMgr * const)353 void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */)
354 {
355 d->m_balanceCache.clear();
356 d->m_priceCache.clear();
357 d->m_storage = nullptr;
358 }
359
storage() const360 MyMoneyStorageMgr* MyMoneyFile::storage() const
361 {
362 return d->m_storage;
363 }
364
storageAttached() const365 bool MyMoneyFile::storageAttached() const
366 {
367 return d->m_storage != 0;
368 }
369
startTransaction()370 void MyMoneyFile::startTransaction()
371 {
372 d->checkStorage();
373 if (d->m_inTransaction) {
374 throw MYMONEYEXCEPTION_CSTRING("Already started a transaction!");
375 }
376
377 d->m_storage->startTransaction();
378 d->m_inTransaction = true;
379 d->m_changeSet.clear();
380 }
381
hasTransaction() const382 bool MyMoneyFile::hasTransaction() const
383 {
384 return d->m_inTransaction;
385 }
386
commitTransaction()387 void MyMoneyFile::commitTransaction()
388 {
389 d->checkTransaction(Q_FUNC_INFO);
390
391 // commit the transaction in the storage
392 const auto changed = d->m_storage->commitTransaction();
393 d->m_inTransaction = false;
394
395 // collect notifications about removed objects
396 QStringList removedObjects;
397 const auto& set = d->m_changeSet;
398 for (const auto& change : set) {
399 switch (change.notificationMode()) {
400 case File::Mode::Remove:
401 removedObjects += change.id();
402 break;
403 default:
404 break;
405 }
406 }
407
408 // inform the outside world about the beginning of notifications
409 emit beginChangeNotification();
410
411 // Now it's time to send out some signals to the outside world
412 // First we go through the d->m_changeSet and emit respective
413 // signals about addition, modification and removal of engine objects
414 const auto& changes = d->m_changeSet;
415 for (const auto& change : changes) {
416 switch (change.notificationMode()) {
417 case File::Mode::Remove:
418 emit objectRemoved(change.objectType(), change.id());
419 // if there is a balance change recorded for this account remove it since the account itself will be removed
420 // this can happen when deleting categories that have transactions and the reassign category feature was used
421 d->m_balanceChangedSet.remove(change.id());
422 break;
423 case File::Mode::Add:
424 if (!removedObjects.contains(change.id())) {
425 emit objectAdded(change.objectType(), change.id());
426 }
427 break;
428 case File::Mode::Modify:
429 if (!removedObjects.contains(change.id())) {
430 emit objectModified(change.objectType(), change.id());
431 }
432 break;
433 }
434 }
435
436 // we're done with the change set, so we clear it
437 d->m_changeSet.clear();
438
439 // now send out the balanceChanged signal for all those
440 // accounts for which we have an indication about a possible
441 // change.
442 const auto& balanceChanges = d->m_balanceChangedSet;
443 for (const auto& id : balanceChanges) {
444 if (!removedObjects.contains(id)) {
445 // if we notify about balance change we don't need to notify about value change
446 // for the same account since a balance change implies a value change
447 d->m_valueChangedSet.remove(id);
448 emit balanceChanged(account(id));
449 }
450 }
451 d->m_balanceChangedSet.clear();
452
453 // now notify about the remaining value changes
454 const auto& m_valueChanges = d->m_valueChangedSet;
455 for (const auto& id : m_valueChanges) {
456 if (!removedObjects.contains(id)) {
457 emit valueChanged(account(id));
458 }
459 }
460
461 d->m_valueChangedSet.clear();
462
463 // as a last action, send out the global dataChanged signal
464 if (changed)
465 emit dataChanged();
466
467 // inform the outside world about the end of notifications
468 emit endChangeNotification();
469 }
470
rollbackTransaction()471 void MyMoneyFile::rollbackTransaction()
472 {
473 d->checkTransaction(Q_FUNC_INFO);
474
475 d->m_storage->rollbackTransaction();
476 d->m_inTransaction = false;
477 d->m_balanceChangedSet.clear();
478 d->m_valueChangedSet.clear();
479 d->m_changeSet.clear();
480 }
481
addInstitution(MyMoneyInstitution & institution)482 void MyMoneyFile::addInstitution(MyMoneyInstitution& institution)
483 {
484 // perform some checks to see that the institution stuff is OK. For
485 // now we assume that the institution must have a name, the ID is not set
486 // and it does not have a parent (MyMoneyFile).
487
488 if (institution.name().length() == 0
489 || institution.id().length() != 0)
490 throw MYMONEYEXCEPTION_CSTRING("Not a new institution");
491
492 d->checkTransaction(Q_FUNC_INFO);
493
494 d->m_storage->addInstitution(institution);
495 d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution);
496 }
497
modifyInstitution(const MyMoneyInstitution & institution)498 void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution)
499 {
500 d->checkTransaction(Q_FUNC_INFO);
501
502 d->m_storage->modifyInstitution(institution);
503 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
504 }
505
modifyTransaction(const MyMoneyTransaction & transaction)506 void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction)
507 {
508 d->checkTransaction(Q_FUNC_INFO);
509
510 MyMoneyTransaction tCopy(transaction);
511
512 // now check the splits
513 bool loanAccountAffected = false;
514 const auto splits1 = transaction.splits();
515 for (const auto& split : splits1) {
516 // the following line will throw an exception if the
517 // account does not exist
518 auto acc = MyMoneyFile::account(split.accountId());
519 if (acc.id().isEmpty())
520 throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned");
521 if (isStandardAccount(split.accountId()))
522 throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account");
523 if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)))
524 loanAccountAffected = true;
525 }
526
527 // change transfer splits between asset/liability and loan accounts
528 // into amortization splits
529 if (loanAccountAffected) {
530 const auto splits = transaction.splits();
531 for (const auto& split : splits) {
532 if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) {
533 auto acc = MyMoneyFile::account(split.accountId());
534
535 if (acc.isAssetLiability()) {
536 MyMoneySplit s = split;
537 s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
538 tCopy.modifySplit(s);
539 }
540 }
541 }
542 }
543
544 // clear all changed objects from cache
545 MyMoneyNotifier notifier(d);
546
547 // get the current setting of this transaction
548 MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id());
549
550 // scan the splits again to update notification list
551 // and mark all accounts that are referenced
552 const auto splits2 = tr.splits();
553 foreach (const auto& split, splits2)
554 d->addCacheNotification(split.accountId(), tr.postDate());
555
556 // make sure the value is rounded to the accounts precision
557 fixSplitPrecision(tCopy);
558
559 // perform modification
560 d->m_storage->modifyTransaction(tCopy);
561
562 // and mark all accounts that are referenced
563 const auto splits3 = tCopy.splits();
564 for (const auto& split : splits3)
565 d->addCacheNotification(split.accountId(), tCopy.postDate());
566
567 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction);
568 }
569
modifyAccount(const MyMoneyAccount & _account)570 void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account)
571 {
572 d->checkTransaction(Q_FUNC_INFO);
573
574 MyMoneyAccount account(_account);
575
576 auto acc = MyMoneyFile::account(account.id());
577
578 // check that for standard accounts only specific parameters are changed
579 if (isStandardAccount(account.id())) {
580 // make sure to use the stuff we found on file
581 account = acc;
582
583 // and only use the changes that are allowed
584 account.setName(_account.name());
585 account.setCurrencyId(_account.currencyId());
586
587 // now check that it is the same
588 if (!(account == _account))
589 throw MYMONEYEXCEPTION_CSTRING("Unable to modify the standard account groups");
590 }
591
592 if (account.accountType() != acc.accountType() &&
593 !account.isLiquidAsset() && !acc.isLiquidAsset())
594 throw MYMONEYEXCEPTION_CSTRING("Unable to change account type");
595
596 // if the account was moved to another institution, we notify
597 // the old one as well as the new one and the structure change
598 if (acc.institutionId() != account.institutionId()) {
599 MyMoneyInstitution inst;
600 if (!acc.institutionId().isEmpty()) {
601 inst = institution(acc.institutionId());
602 inst.removeAccountId(acc.id());
603 modifyInstitution(inst);
604 // modifyInstitution updates d->m_changeSet already
605 }
606 if (!account.institutionId().isEmpty()) {
607 inst = institution(account.institutionId());
608 inst.addAccountId(acc.id());
609 modifyInstitution(inst);
610 // modifyInstitution updates d->m_changeSet already
611 }
612 }
613
614 d->m_storage->modifyAccount(account);
615 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account);
616 }
617
reparentAccount(MyMoneyAccount & acc,MyMoneyAccount & parent)618 void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent)
619 {
620 d->checkTransaction(Q_FUNC_INFO);
621
622 // check that it's not one of the standard account groups
623 if (isStandardAccount(acc.id()))
624 throw MYMONEYEXCEPTION_CSTRING("Unable to reparent the standard account groups");
625
626 if (acc.accountGroup() == parent.accountGroup()
627 || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense)
628 || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) {
629
630 if (acc.isInvest() && parent.accountType() != Account::Type::Investment)
631 throw MYMONEYEXCEPTION_CSTRING("Unable to reparent Stock to non-investment account");
632
633 if (parent.accountType() == Account::Type::Investment && !acc.isInvest())
634 throw MYMONEYEXCEPTION_CSTRING("Unable to reparent non-stock to investment account");
635
636 // keep a notification of the current parent
637 MyMoneyAccount curParent = account(acc.parentAccountId());
638
639 d->m_storage->reparentAccount(acc, parent);
640
641 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent);
642 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
643 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc);
644
645 } else
646 throw MYMONEYEXCEPTION_CSTRING("Unable to reparent to different account type");
647 }
648
institution(const QString & id) const649 MyMoneyInstitution MyMoneyFile::institution(const QString& id) const
650 {
651 return d->m_storage->institution(id);
652 }
653
account(const QString & id) const654 MyMoneyAccount MyMoneyFile::account(const QString& id) const
655 {
656 if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id
657 return MyMoneyAccount();
658
659 return d->m_storage->account(id);
660 }
661
subAccountByName(const MyMoneyAccount & account,const QString & name) const662 MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const
663 {
664 static MyMoneyAccount nullAccount;
665
666 const auto accounts = account.accountList();
667 for (const auto& acc : accounts) {
668 const auto sacc = MyMoneyFile::account(acc);
669 if (sacc.name().compare(name) == 0)
670 return sacc;
671 }
672 return nullAccount;
673 }
674
accountByName(const QString & name) const675 MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const
676 {
677 try {
678 return d->m_storage->accountByName(name);
679 } catch (const MyMoneyException &) {
680 }
681 return MyMoneyAccount();
682 }
683
removeTransaction(const MyMoneyTransaction & transaction)684 void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction)
685 {
686 d->checkTransaction(Q_FUNC_INFO);
687
688 // clear all changed objects from cache
689 MyMoneyNotifier notifier(d);
690
691 // get the engine's idea about this transaction
692 MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id());
693
694 // scan the splits again to update notification list
695 const auto splits = tr.splits();
696 for (const auto& split : splits) {
697 auto acc = account(split.accountId());
698 if (acc.isClosed())
699 throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove transaction that references a closed account."));
700 d->addCacheNotification(split.accountId(), tr.postDate());
701 //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ??
702 }
703
704 d->m_storage->removeTransaction(transaction);
705
706 // remove a possible notification of that same object from the changeSet
707 QList<MyMoneyNotification>::iterator it;
708 for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) {
709 if((*it).id() == transaction.id()) {
710 it = d->m_changeSet.erase(it);
711 } else {
712 ++it;
713 }
714 }
715
716 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction);
717 }
718
719
hasActiveSplits(const QString & id) const720 bool MyMoneyFile::hasActiveSplits(const QString& id) const
721 {
722 d->checkStorage();
723
724 return d->m_storage->hasActiveSplits(id);
725 }
726
isStandardAccount(const QString & id) const727 bool MyMoneyFile::isStandardAccount(const QString& id) const
728 {
729 d->checkStorage();
730
731 return d->m_storage->isStandardAccount(id);
732 }
733
setAccountName(const QString & id,const QString & name) const734 void MyMoneyFile::setAccountName(const QString& id, const QString& name) const
735 {
736 d->checkTransaction(Q_FUNC_INFO);
737
738 auto acc = account(id);
739 d->m_storage->setAccountName(id, name);
740 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc);
741 }
742
removeAccount(const MyMoneyAccount & account)743 void MyMoneyFile::removeAccount(const MyMoneyAccount& account)
744 {
745 d->checkTransaction(Q_FUNC_INFO);
746
747 MyMoneyAccount parent;
748 MyMoneyAccount acc;
749 MyMoneyInstitution institution;
750
751 // check that the account and its parent exist
752 // this will throw an exception if the id is unknown
753 acc = MyMoneyFile::account(account.id());
754 parent = MyMoneyFile::account(account.parentAccountId());
755 if (!acc.institutionId().isEmpty())
756 institution = MyMoneyFile::institution(acc.institutionId());
757
758 // check that it's not one of the standard account groups
759 if (isStandardAccount(account.id()))
760 throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups");
761
762 if (hasActiveSplits(account.id())) {
763 throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits");
764 }
765
766 // collect all sub-ordinate accounts for notification
767 const auto accounts = acc.accountList();
768 for (const auto& id : accounts)
769 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id));
770
771 // don't forget the parent and a possible institution
772
773 if (!institution.id().isEmpty()) {
774 institution.removeAccountId(account.id());
775 d->m_storage->modifyInstitution(institution);
776 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
777 }
778 acc.setInstitutionId(QString());
779
780 d->m_storage->removeAccount(acc);
781
782 d->m_balanceCache.clear(acc.id());
783
784 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
785 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc);
786 }
787
removeAccountList(const QStringList & account_list,unsigned int level)788 void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level)
789 {
790 if (level > 100)
791 throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::removeAccountList]!");
792
793 d->checkTransaction(Q_FUNC_INFO);
794
795 // upon entry, we check that we could proceed with the operation
796 if (!level) {
797 if (!hasOnlyUnusedAccounts(account_list, 0)) {
798 throw MYMONEYEXCEPTION_CSTRING("One or more accounts cannot be removed");
799 }
800 }
801
802 // process all accounts in the list and test if they have transactions assigned
803 foreach (const auto sAccount, account_list) {
804 auto a = d->m_storage->account(sAccount);
805 //qDebug() << "Deleting account '"<< a.name() << "'";
806
807 // first remove all sub-accounts
808 if (!a.accountList().isEmpty()) {
809 removeAccountList(a.accountList(), level + 1);
810
811 // then remove account itself, but we first have to get
812 // rid of the account list that is still stored in
813 // the MyMoneyAccount object. Easiest way is to get a fresh copy.
814 a = d->m_storage->account(sAccount);
815 }
816
817 // make sure to remove the item from the cache
818 removeAccount(a);
819 }
820 }
821
hasOnlyUnusedAccounts(const QStringList & account_list,unsigned int level)822 bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level)
823 {
824 if (level > 100)
825 throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!");
826 // process all accounts in the list and test if they have transactions assigned
827 for (const auto& sAccount : account_list) {
828 if (transactionCount(sAccount) != 0)
829 return false; // the current account has a transaction assigned
830 if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1))
831 return false; // some sub-account has a transaction assigned
832 }
833 return true; // all subaccounts unused
834 }
835
836
removeInstitution(const MyMoneyInstitution & institution)837 void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution)
838 {
839 d->checkTransaction(Q_FUNC_INFO);
840
841 MyMoneyInstitution inst = MyMoneyFile::institution(institution.id());
842
843 bool blocked = signalsBlocked();
844 blockSignals(true);
845 const auto accounts = inst.accountList();
846 for (const auto& acc : accounts) {
847 auto a = account(acc);
848 a.setInstitutionId(QString());
849 modifyAccount(a);
850 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a);
851 }
852 blockSignals(blocked);
853
854 d->m_storage->removeInstitution(institution);
855
856 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution);
857 }
858
createAccount(MyMoneyAccount & newAccount,MyMoneyAccount & parentAccount,MyMoneyAccount & brokerageAccount,MyMoneyMoney openingBal)859 void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal)
860 {
861 // make sure we have a currency. If none is assigned, we assume base currency
862 if (newAccount.currencyId().isEmpty())
863 newAccount.setCurrencyId(baseCurrency().id());
864
865 MyMoneyFileTransaction ft;
866 try {
867 int pos;
868 // check for ':' in the name and use it as separator for a hierarchy
869 while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) {
870 QString part = newAccount.name().left(pos);
871 QString remainder = newAccount.name().mid(pos + 1);
872 const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part);
873 if (existingAccount.id().isEmpty()) {
874 newAccount.setName(part);
875
876 addAccount(newAccount, parentAccount);
877 parentAccount = newAccount;
878 } else {
879 parentAccount = existingAccount;
880 }
881 newAccount.setParentAccountId(QString()); // make sure, there's no parent
882 newAccount.clearId(); // and no id set for adding
883 newAccount.removeAccountIds(); // and no sub-account ids
884 newAccount.setName(remainder);
885 }
886
887 addAccount(newAccount, parentAccount);
888
889 // in case of a loan account, we add the initial payment
890 if ((newAccount.accountType() == Account::Type::Loan
891 || newAccount.accountType() == Account::Type::AssetLoan)
892 && !newAccount.value("kmm-loan-payment-acc").isEmpty()
893 && !newAccount.value("kmm-loan-payment-date").isEmpty()) {
894 MyMoneyAccountLoan acc(newAccount);
895 MyMoneyTransaction t;
896 MyMoneySplit a, b;
897 a.setAccountId(acc.id());
898 b.setAccountId(acc.value("kmm-loan-payment-acc"));
899 a.setValue(acc.loanAmount());
900 if (acc.accountType() == Account::Type::Loan)
901 a.setValue(-a.value());
902
903 a.setShares(a.value());
904 b.setValue(-a.value());
905 b.setShares(b.value());
906 a.setMemo(i18n("Loan payout"));
907 b.setMemo(i18n("Loan payout"));
908 t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate));
909 newAccount.deletePair("kmm-loan-payment-acc");
910 newAccount.deletePair("kmm-loan-payment-date");
911 MyMoneyFile::instance()->modifyAccount(newAccount);
912
913 t.addSplit(a);
914 t.addSplit(b);
915 addTransaction(t);
916 createOpeningBalanceTransaction(newAccount, openingBal);
917
918 // in case of an investment account we check if we should create
919 // a brokerage account
920 } else if (newAccount.accountType() == Account::Type::Investment
921 && !brokerageAccount.name().isEmpty()) {
922 addAccount(brokerageAccount, parentAccount);
923
924 // set a link from the investment account to the brokerage account
925 modifyAccount(newAccount);
926 createOpeningBalanceTransaction(brokerageAccount, openingBal);
927
928 } else
929 createOpeningBalanceTransaction(newAccount, openingBal);
930
931 ft.commit();
932 } catch (const MyMoneyException &e) {
933 qWarning("Unable to create account: %s", e.what());
934 throw;
935 }
936 }
937
addAccount(MyMoneyAccount & account,MyMoneyAccount & parent)938 void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent)
939 {
940 d->checkTransaction(Q_FUNC_INFO);
941
942 MyMoneyInstitution institution;
943
944 // perform some checks to see that the account stuff is OK. For
945 // now we assume that the account must have a name, has no
946 // transaction and sub-accounts and parent account
947 // it's own ID is not set and it does not have a pointer to (MyMoneyFile)
948
949 if (account.name().length() == 0)
950 throw MYMONEYEXCEPTION_CSTRING("Account has no name");
951
952 if (account.id().length() != 0)
953 throw MYMONEYEXCEPTION_CSTRING("New account must have no id");
954
955 if (account.accountList().count() != 0)
956 throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts");
957
958 if (!account.parentAccountId().isEmpty())
959 throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id");
960
961 if (account.accountType() == Account::Type::Unknown)
962 throw MYMONEYEXCEPTION_CSTRING("Account has invalid type");
963
964 // make sure, that the parent account exists
965 // if not, an exception is thrown. If it exists,
966 // get a copy of the current data
967 auto acc = MyMoneyFile::account(parent.id());
968
969 #if 0
970 // TODO: remove the following code as we now can have multiple accounts
971 // with the same name even in the same hierarchy position of the account tree
972 //
973 // check if the selected name is currently not among the child accounts
974 // if we find one, then return it as the new account
975 QStringList::const_iterator it_a;
976 foreach (const auto accountID, acc.accountList()) {
977 MyMoneyAccount a = MyMoneyFile::account(accountID);
978 if (account.name() == a.name()) {
979 account = a;
980 return;
981 }
982 }
983 #endif
984
985 // FIXME: make sure, that the parent has the same type
986 // I left it out here because I don't know, if there is
987 // a tight coupling between e.g. checking accounts and the
988 // class asset. It certainly does not make sense to create an
989 // expense account under an income account. Maybe it does, I don't know.
990
991 // We enforce, that a stock account can never be a parent and
992 // that the parent for a stock account must be an investment. Also,
993 // an investment cannot have another investment account as it's parent
994 if (parent.isInvest())
995 throw MYMONEYEXCEPTION_CSTRING("Stock account cannot be parent account");
996
997 if (account.isInvest() && parent.accountType() != Account::Type::Investment)
998 throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent ");
999
1000 if (!account.isInvest() && parent.accountType() == Account::Type::Investment)
1001 throw MYMONEYEXCEPTION_CSTRING("Investment account can only have stock accounts as children");
1002
1003 // if an institution is set, verify that it exists
1004 if (account.institutionId().length() != 0) {
1005 // check the presence of the institution. if it
1006 // does not exist, an exception is thrown
1007 institution = MyMoneyFile::institution(account.institutionId());
1008 }
1009
1010 // if we don't have a valid opening date use today
1011 if (!account.openingDate().isValid()) {
1012 account.setOpeningDate(QDate::currentDate());
1013 }
1014
1015 // make sure to set the opening date for categories to a
1016 // fixed date (1900-1-1). See #313793 on b.k.o for details
1017 if (account.isIncomeExpense()) {
1018 account.setOpeningDate(QDate(1900, 1, 1));
1019 }
1020
1021 // if we don't have a currency assigned use the base currency
1022 if (account.currencyId().isEmpty()) {
1023 account.setCurrencyId(baseCurrency().id());
1024 }
1025
1026 // make sure the parent id is setup
1027 account.setParentAccountId(parent.id());
1028
1029 d->m_storage->addAccount(account);
1030 d->m_changeSet += MyMoneyNotification(File::Mode::Add, account);
1031
1032 d->m_storage->addAccount(parent, account);
1033 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
1034
1035 if (account.institutionId().length() != 0) {
1036 institution.addAccountId(account.id());
1037 d->m_storage->modifyInstitution(institution);
1038 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
1039 }
1040 }
1041
createOpeningBalanceTransaction(const MyMoneyAccount & acc,const MyMoneyMoney & balance)1042 MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance)
1043 {
1044 MyMoneyTransaction t;
1045 // if the opening balance is not zero, we need
1046 // to create the respective transaction
1047 if (!balance.isZero()) {
1048 d->checkTransaction(Q_FUNC_INFO);
1049
1050 MyMoneySecurity currency = security(acc.currencyId());
1051 MyMoneyAccount openAcc = openingBalanceAccount(currency);
1052
1053 if (openAcc.openingDate() > acc.openingDate()) {
1054 openAcc.setOpeningDate(acc.openingDate());
1055 modifyAccount(openAcc);
1056 }
1057
1058 MyMoneySplit s;
1059
1060 t.setPostDate(acc.openingDate());
1061 t.setCommodity(acc.currencyId());
1062
1063 s.setAccountId(acc.id());
1064 s.setShares(balance);
1065 s.setValue(balance);
1066 t.addSplit(s);
1067
1068 s.clearId();
1069 s.setAccountId(openAcc.id());
1070 s.setShares(-balance);
1071 s.setValue(-balance);
1072 t.addSplit(s);
1073
1074 addTransaction(t);
1075 }
1076 return t;
1077 }
1078
openingBalanceTransaction(const MyMoneyAccount & acc) const1079 QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const
1080 {
1081 QString result;
1082
1083 MyMoneySecurity currency = security(acc.currencyId());
1084 MyMoneyAccount openAcc;
1085
1086 try {
1087 openAcc = openingBalanceAccount(currency);
1088 } catch (const MyMoneyException &) {
1089 return result;
1090 }
1091
1092 // Iterate over all the opening balance transactions for this currency
1093 MyMoneyTransactionFilter filter;
1094 filter.addAccount(openAcc.id());
1095 QList<MyMoneyTransaction> transactions = transactionList(filter);
1096 QList<MyMoneyTransaction>::const_iterator it_t = transactions.constBegin();
1097 while (it_t != transactions.constEnd()) {
1098 try {
1099 // Test whether the transaction also includes a split into
1100 // this account
1101 (*it_t).splitByAccount(acc.id(), true /*match*/);
1102
1103 // If so, we have a winner!
1104 result = (*it_t).id();
1105 break;
1106 } catch (const MyMoneyException &) {
1107 // If not, keep searching
1108 ++it_t;
1109 }
1110 }
1111
1112 return result;
1113 }
1114
openingBalanceAccount(const MyMoneySecurity & security)1115 MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security)
1116 {
1117 if (!security.isCurrency())
1118 throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported");
1119
1120 try {
1121 return openingBalanceAccount_internal(security);
1122 } catch (const MyMoneyException &) {
1123 MyMoneyFileTransaction ft;
1124 MyMoneyAccount acc;
1125
1126 try {
1127 acc = createOpeningBalanceAccount(security);
1128 ft.commit();
1129
1130 } catch (const MyMoneyException &) {
1131 qDebug("Unable to create opening balance account for security %s", qPrintable(security.id()));
1132 }
1133 return acc;
1134 }
1135 }
1136
openingBalanceAccount(const MyMoneySecurity & security) const1137 MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const
1138 {
1139 return openingBalanceAccount_internal(security);
1140 }
1141
openingBalanceAccount_internal(const MyMoneySecurity & security) const1142 MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const
1143 {
1144 if (!security.isCurrency())
1145 throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported");
1146
1147 MyMoneyAccount acc;
1148 QList<MyMoneyAccount> accounts;
1149 QList<MyMoneyAccount>::ConstIterator it;
1150
1151 accountList(accounts, equity().accountList(), true);
1152
1153 for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1154 if (it->value("OpeningBalanceAccount") == QLatin1String("Yes")
1155 && it->currencyId() == security.id()) {
1156 acc = *it;
1157 break;
1158 }
1159 }
1160
1161 if (acc.id().isEmpty()) {
1162 for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1163 if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix())
1164 && it->currencyId() == security.id()) {
1165 acc = *it;
1166 break;
1167 }
1168 }
1169 }
1170
1171 if (acc.id().isEmpty())
1172 throw MYMONEYEXCEPTION(QString::fromLatin1("No opening balance account for %1").arg(security.tradingSymbol()));
1173
1174 return acc;
1175 }
1176
createOpeningBalanceAccount(const MyMoneySecurity & security)1177 MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security)
1178 {
1179 d->checkTransaction(Q_FUNC_INFO);
1180
1181 MyMoneyAccount acc;
1182 QList<MyMoneyAccount> accounts;
1183 QList<MyMoneyAccount>::ConstIterator it;
1184
1185 accountList(accounts, equity().accountList(), true);
1186
1187 // find present opening balance accounts without containing '('
1188 QString name;
1189 QString parentAccountId;
1190 QRegExp exp(QString("\\([A-Z]{3}\\)"));
1191
1192 for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1193 if (it->value("OpeningBalanceAccount") == QLatin1String("Yes")
1194 && exp.indexIn(it->name()) == -1) {
1195 name = it->name();
1196 parentAccountId = it->parentAccountId();
1197 break;
1198 }
1199 }
1200
1201 if (name.isEmpty())
1202 name = MyMoneyFile::openingBalancesPrefix();
1203 if (security.id() != baseCurrency().id()) {
1204 name += QString(" (%1)").arg(security.id());
1205 }
1206 acc.setName(name);
1207 acc.setAccountType(Account::Type::Equity);
1208 acc.setCurrencyId(security.id());
1209 acc.setValue("OpeningBalanceAccount", "Yes");
1210
1211 MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity();
1212 this->addAccount(acc, parent);
1213 return acc;
1214 }
1215
addTransaction(MyMoneyTransaction & transaction)1216 void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction)
1217 {
1218 d->checkTransaction(Q_FUNC_INFO);
1219
1220 // clear all changed objects from cache
1221 MyMoneyNotifier notifier(d);
1222
1223 // perform some checks to see that the transaction stuff is OK. For
1224 // now we assume that
1225 // * no ids are assigned
1226 // * the date valid (must not be empty)
1227 // * the referenced accounts in the splits exist
1228
1229 // first perform all the checks
1230 if (!transaction.id().isEmpty())
1231 throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with id set");
1232 if (!transaction.postDate().isValid())
1233 throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with invalid postdate");
1234
1235 // now check the splits
1236 auto loanAccountAffected = false;
1237 const auto splits1 = transaction.splits();
1238 for (const auto& split : splits1) {
1239 // the following line will throw an exception if the
1240 // account does not exist or is one of the standard accounts
1241 auto acc = MyMoneyFile::account(split.accountId());
1242 if (acc.id().isEmpty())
1243 throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned");
1244 if (acc.isLoan())
1245 loanAccountAffected = true;
1246 if (isStandardAccount(split.accountId()))
1247 throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account");
1248 }
1249
1250 // change transfer splits between asset/liability and loan accounts
1251 // into amortization splits
1252 if (loanAccountAffected) {
1253 foreach (const auto split, transaction.splits()) {
1254 if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) {
1255 auto acc = MyMoneyFile::account(split.accountId());
1256
1257 if (acc.isAssetLiability()) {
1258 MyMoneySplit s = split;
1259 s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
1260 transaction.modifySplit(s);
1261 }
1262 }
1263 }
1264 }
1265
1266 // check that we have a commodity
1267 if (transaction.commodity().isEmpty()) {
1268 transaction.setCommodity(baseCurrency().id());
1269 }
1270
1271 // make sure the value is rounded to the accounts precision
1272 fixSplitPrecision(transaction);
1273
1274 // then add the transaction to the file global pool
1275 d->m_storage->addTransaction(transaction);
1276
1277 // scan the splits again to update notification list
1278 const auto splits2 = transaction.splits();
1279 for (const auto& split : splits2)
1280 d->addCacheNotification(split.accountId(), transaction.postDate());
1281
1282 d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction);
1283 }
1284
transaction(const QString & id) const1285 MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const
1286 {
1287 d->checkStorage();
1288
1289 return d->m_storage->transaction(id);
1290 }
1291
transaction(const QString & account,const int idx) const1292 MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const
1293 {
1294 d->checkStorage();
1295
1296 return d->m_storage->transaction(account, idx);
1297 }
1298
addPayee(MyMoneyPayee & payee)1299 void MyMoneyFile::addPayee(MyMoneyPayee& payee)
1300 {
1301 d->checkTransaction(Q_FUNC_INFO);
1302
1303 d->m_storage->addPayee(payee);
1304 d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee);
1305 }
1306
payee(const QString & id) const1307 MyMoneyPayee MyMoneyFile::payee(const QString& id) const
1308 {
1309 if (Q_UNLIKELY(id.isEmpty()))
1310 return MyMoneyPayee();
1311
1312 return d->m_storage->payee(id);
1313 }
1314
payeeByName(const QString & name) const1315 MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const
1316 {
1317 d->checkStorage();
1318
1319 return d->m_storage->payeeByName(name);
1320 }
1321
modifyPayee(const MyMoneyPayee & payee)1322 void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee)
1323 {
1324 d->checkTransaction(Q_FUNC_INFO);
1325
1326 d->m_storage->modifyPayee(payee);
1327 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee);
1328 }
1329
removePayee(const MyMoneyPayee & payee)1330 void MyMoneyFile::removePayee(const MyMoneyPayee& payee)
1331 {
1332 d->checkTransaction(Q_FUNC_INFO);
1333
1334 // FIXME we need to make sure, that the payee is not referenced anymore
1335 d->m_storage->removePayee(payee);
1336 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee);
1337 }
1338
addTag(MyMoneyTag & tag)1339 void MyMoneyFile::addTag(MyMoneyTag& tag)
1340 {
1341 d->checkTransaction(Q_FUNC_INFO);
1342
1343 d->m_storage->addTag(tag);
1344 d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag);
1345 }
1346
tag(const QString & id) const1347 MyMoneyTag MyMoneyFile::tag(const QString& id) const
1348 {
1349 return d->m_storage->tag(id);
1350 }
1351
tagByName(const QString & name) const1352 MyMoneyTag MyMoneyFile::tagByName(const QString& name) const
1353 {
1354 d->checkStorage();
1355
1356 return d->m_storage->tagByName(name);
1357 }
1358
modifyTag(const MyMoneyTag & tag)1359 void MyMoneyFile::modifyTag(const MyMoneyTag& tag)
1360 {
1361 d->checkTransaction(Q_FUNC_INFO);
1362
1363 d->m_storage->modifyTag(tag);
1364 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag);
1365 }
1366
removeTag(const MyMoneyTag & tag)1367 void MyMoneyFile::removeTag(const MyMoneyTag& tag)
1368 {
1369 d->checkTransaction(Q_FUNC_INFO);
1370
1371 // FIXME we need to make sure, that the tag is not referenced anymore
1372 d->m_storage->removeTag(tag);
1373 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag);
1374 }
1375
accountList(QList<MyMoneyAccount> & list,const QStringList & idlist,const bool recursive) const1376 void MyMoneyFile::accountList(QList<MyMoneyAccount>& list, const QStringList& idlist, const bool recursive) const
1377 {
1378 d->checkStorage();
1379
1380 if (idlist.isEmpty()) {
1381 d->m_storage->accountList(list);
1382
1383 #if 0
1384 // TODO: I have no idea what this was good for, but it caused the networth report
1385 // to show double the numbers so I commented it out (ipwizard, 2008-05-24)
1386 if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) {
1387 d->m_storage->accountList(list);
1388 d->m_cache.preloadAccount(list);
1389 }
1390 #endif
1391
1392 QList<MyMoneyAccount>::Iterator it;
1393 for (it = list.begin(); it != list.end();) {
1394 if (isStandardAccount((*it).id())) {
1395 it = list.erase(it);
1396 } else {
1397 ++it;
1398 }
1399 }
1400 } else {
1401 QList<MyMoneyAccount>::ConstIterator it;
1402 QList<MyMoneyAccount> list_a;
1403 d->m_storage->accountList(list_a);
1404
1405 for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) {
1406 if (!isStandardAccount((*it).id())) {
1407 if (idlist.indexOf((*it).id()) != -1) {
1408 list.append(*it);
1409 if (recursive == true && !(*it).accountList().isEmpty()) {
1410 accountList(list, (*it).accountList(), true);
1411 }
1412 }
1413 }
1414 }
1415 }
1416 }
1417
institutionList() const1418 QList<MyMoneyInstitution> MyMoneyFile::institutionList() const
1419 {
1420 return d->m_storage->institutionList();
1421 }
1422
1423 // general get functions
user() const1424 MyMoneyPayee MyMoneyFile::user() const
1425 {
1426 d->checkStorage();
1427 return d->m_storage->user();
1428 }
1429
1430 // general set functions
setUser(const MyMoneyPayee & user)1431 void MyMoneyFile::setUser(const MyMoneyPayee& user)
1432 {
1433 d->checkTransaction(Q_FUNC_INFO);
1434
1435 d->m_storage->setUser(user);
1436 }
1437
dirty() const1438 bool MyMoneyFile::dirty() const
1439 {
1440 if (!d->m_storage)
1441 return false;
1442
1443 return d->m_storage->dirty();
1444 }
1445
setDirty() const1446 void MyMoneyFile::setDirty() const
1447 {
1448 d->checkStorage();
1449
1450 d->m_storage->setDirty();
1451 }
1452
accountCount() const1453 unsigned int MyMoneyFile::accountCount() const
1454 {
1455 d->checkStorage();
1456
1457 return d->m_storage->accountCount();
1458 }
1459
ensureDefaultCurrency(MyMoneyAccount & acc) const1460 void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const
1461 {
1462 if (acc.currencyId().isEmpty()) {
1463 if (!baseCurrency().id().isEmpty())
1464 acc.setCurrencyId(baseCurrency().id());
1465 }
1466 }
1467
liability() const1468 MyMoneyAccount MyMoneyFile::liability() const
1469 {
1470 d->checkStorage();
1471
1472 return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability));
1473 }
1474
asset() const1475 MyMoneyAccount MyMoneyFile::asset() const
1476 {
1477 d->checkStorage();
1478
1479 return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset));
1480 }
1481
expense() const1482 MyMoneyAccount MyMoneyFile::expense() const
1483 {
1484 d->checkStorage();
1485
1486 return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense));
1487 }
1488
income() const1489 MyMoneyAccount MyMoneyFile::income() const
1490 {
1491 d->checkStorage();
1492
1493 return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income));
1494 }
1495
equity() const1496 MyMoneyAccount MyMoneyFile::equity() const
1497 {
1498 d->checkStorage();
1499
1500 return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity));
1501 }
1502
transactionCount(const QString & account) const1503 unsigned int MyMoneyFile::transactionCount(const QString& account) const
1504 {
1505 d->checkStorage();
1506
1507 return d->m_storage->transactionCount(account);
1508 }
1509
transactionCount() const1510 unsigned int MyMoneyFile::transactionCount() const
1511 {
1512 return transactionCount(QString());
1513 }
1514
transactionCountMap() const1515 QMap<QString, unsigned long> MyMoneyFile::transactionCountMap() const
1516 {
1517 d->checkStorage();
1518
1519 return d->m_storage->transactionCountMap();
1520 }
1521
institutionCount() const1522 unsigned int MyMoneyFile::institutionCount() const
1523 {
1524 d->checkStorage();
1525
1526 return d->m_storage->institutionCount();
1527 }
1528
balance(const QString & id,const QDate & date) const1529 MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const
1530 {
1531 if (date.isValid()) {
1532 MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date);
1533 if (bal.isValid())
1534 return bal.balance();
1535 }
1536
1537 d->checkStorage();
1538
1539 MyMoneyMoney returnValue = d->m_storage->balance(id, date);
1540
1541 if (date.isValid()) {
1542 d->m_balanceCache.insert(id, date, returnValue);
1543 }
1544
1545 return returnValue;
1546 }
1547
balance(const QString & id) const1548 MyMoneyMoney MyMoneyFile::balance(const QString& id) const
1549 {
1550 return balance(id, QDate());
1551 }
1552
clearedBalance(const QString & id,const QDate & date) const1553 MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const
1554 {
1555 MyMoneyMoney cleared;
1556 QList<MyMoneyTransaction> list;
1557
1558 cleared = balance(id, date);
1559
1560 MyMoneyAccount account = this->account(id);
1561 MyMoneyMoney factor(1, 1);
1562 if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity)
1563 factor = -factor;
1564
1565 MyMoneyTransactionFilter filter;
1566 filter.addAccount(id);
1567 filter.setDateFilter(QDate(), date);
1568 filter.setReportAllSplits(false);
1569 filter.addState((int)TransactionFilter::State::NotReconciled);
1570 transactionList(list, filter);
1571
1572 for (QList<MyMoneyTransaction>::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) {
1573 const QList<MyMoneySplit>& splits = (*it_t).splits();
1574 for (QList<MyMoneySplit>::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) {
1575 const MyMoneySplit &split = (*it_s);
1576 if (split.accountId() != id)
1577 continue;
1578 cleared -= split.shares();
1579 }
1580 }
1581 return cleared * factor;
1582 }
1583
totalBalance(const QString & id,const QDate & date) const1584 MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const
1585 {
1586 d->checkStorage();
1587
1588 return d->m_storage->totalBalance(id, date);
1589 }
1590
totalBalance(const QString & id) const1591 MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const
1592 {
1593 return totalBalance(id, QDate());
1594 }
1595
warningMissingRate(const QString & fromId,const QString & toId) const1596 void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const
1597 {
1598 MyMoneySecurity from, to;
1599 try {
1600 from = security(fromId);
1601 to = security(toId);
1602 qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name()));
1603
1604 } catch (const MyMoneyException &e) {
1605 qWarning("Missing security caught in MyMoneyFile::warningMissingRate(). %s", e.what());
1606 }
1607 }
1608
transactionList(QList<QPair<MyMoneyTransaction,MyMoneySplit>> & list,MyMoneyTransactionFilter & filter) const1609 void MyMoneyFile::transactionList(QList<QPair<MyMoneyTransaction, MyMoneySplit> >& list, MyMoneyTransactionFilter& filter) const
1610 {
1611 d->checkStorage();
1612 d->m_storage->transactionList(list, filter);
1613 }
1614
transactionList(QList<MyMoneyTransaction> & list,MyMoneyTransactionFilter & filter) const1615 void MyMoneyFile::transactionList(QList<MyMoneyTransaction>& list, MyMoneyTransactionFilter& filter) const
1616 {
1617 d->checkStorage();
1618 d->m_storage->transactionList(list, filter);
1619 }
1620
transactionList(MyMoneyTransactionFilter & filter) const1621 QList<MyMoneyTransaction> MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const
1622 {
1623 d->checkStorage();
1624 return d->m_storage->transactionList(filter);
1625 }
1626
payeeList() const1627 QList<MyMoneyPayee> MyMoneyFile::payeeList() const
1628 {
1629 return d->m_storage->payeeList();
1630 }
1631
tagList() const1632 QList<MyMoneyTag> MyMoneyFile::tagList() const
1633 {
1634 return d->m_storage->tagList();
1635 }
1636
accountToCategory(const QString & accountId,bool includeStandardAccounts) const1637 QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const
1638 {
1639 MyMoneyAccount acc;
1640 QString rc;
1641
1642 if (!accountId.isEmpty()) {
1643 acc = account(accountId);
1644 do {
1645 if (!rc.isEmpty())
1646 rc = AccountSeparator + rc;
1647 rc = acc.name() + rc;
1648 acc = account(acc.parentAccountId());
1649 } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id())));
1650 }
1651 return rc;
1652 }
1653
categoryToAccount(const QString & category,Account::Type type) const1654 QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const
1655 {
1656 QString id;
1657
1658 // search the category in the expense accounts and if it is not found, try
1659 // to locate it in the income accounts
1660 if (type == Account::Type::Unknown
1661 || type == Account::Type::Expense) {
1662 id = locateSubAccount(MyMoneyFile::instance()->expense(), category);
1663 }
1664
1665 if ((id.isEmpty() && type == Account::Type::Unknown)
1666 || type == Account::Type::Income) {
1667 id = locateSubAccount(MyMoneyFile::instance()->income(), category);
1668 }
1669
1670 return id;
1671 }
1672
categoryToAccount(const QString & category) const1673 QString MyMoneyFile::categoryToAccount(const QString& category) const
1674 {
1675 return categoryToAccount(category, Account::Type::Unknown);
1676 }
1677
nameToAccount(const QString & name) const1678 QString MyMoneyFile::nameToAccount(const QString& name) const
1679 {
1680 QString id;
1681
1682 // search the category in the asset accounts and if it is not found, try
1683 // to locate it in the liability accounts
1684 id = locateSubAccount(MyMoneyFile::instance()->asset(), name);
1685 if (id.isEmpty())
1686 id = locateSubAccount(MyMoneyFile::instance()->liability(), name);
1687
1688 return id;
1689 }
1690
parentName(const QString & name) const1691 QString MyMoneyFile::parentName(const QString& name) const
1692 {
1693 return name.section(AccountSeparator, 0, -2);
1694 }
1695
locateSubAccount(const MyMoneyAccount & base,const QString & category) const1696 QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const
1697 {
1698 MyMoneyAccount nextBase;
1699 QString level, remainder;
1700 level = category.section(AccountSeparator, 0, 0);
1701 remainder = category.section(AccountSeparator, 1);
1702
1703 foreach (const auto sAccount, base.accountList()) {
1704 nextBase = account(sAccount);
1705 if (nextBase.name() == level) {
1706 if (remainder.isEmpty()) {
1707 return nextBase.id();
1708 }
1709 return locateSubAccount(nextBase, remainder);
1710 }
1711 }
1712 return QString();
1713 }
1714
value(const QString & key) const1715 QString MyMoneyFile::value(const QString& key) const
1716 {
1717 d->checkStorage();
1718
1719 return d->m_storage->value(key);
1720 }
1721
setValue(const QString & key,const QString & val)1722 void MyMoneyFile::setValue(const QString& key, const QString& val)
1723 {
1724 d->checkTransaction(Q_FUNC_INFO);
1725
1726 d->m_storage->setValue(key, val);
1727 }
1728
deletePair(const QString & key)1729 void MyMoneyFile::deletePair(const QString& key)
1730 {
1731 d->checkTransaction(Q_FUNC_INFO);
1732
1733 d->m_storage->deletePair(key);
1734 }
1735
addSchedule(MyMoneySchedule & sched)1736 void MyMoneyFile::addSchedule(MyMoneySchedule& sched)
1737 {
1738 d->checkTransaction(Q_FUNC_INFO);
1739
1740 const auto splits = sched.transaction().splits();
1741 for (const auto& split : splits) {
1742 // the following line will throw an exception if the
1743 // account does not exist or is one of the standard accounts
1744 const auto acc = account(split.accountId());
1745 if (acc.id().isEmpty())
1746 throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned");
1747 if (isStandardAccount(split.accountId()))
1748 throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account");
1749 }
1750
1751 d->m_storage->addSchedule(sched);
1752 d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched);
1753 }
1754
modifySchedule(const MyMoneySchedule & sched)1755 void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched)
1756 {
1757 d->checkTransaction(Q_FUNC_INFO);
1758
1759 foreach (const auto split, sched.transaction().splits()) {
1760 // the following line will throw an exception if the
1761 // account does not exist or is one of the standard accounts
1762 auto acc = MyMoneyFile::account(split.accountId());
1763 if (acc.id().isEmpty())
1764 throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned");
1765 if (isStandardAccount(split.accountId()))
1766 throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account");
1767 }
1768
1769 d->m_storage->modifySchedule(sched);
1770 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched);
1771 }
1772
removeSchedule(const MyMoneySchedule & sched)1773 void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched)
1774 {
1775 d->checkTransaction(Q_FUNC_INFO);
1776
1777 d->m_storage->removeSchedule(sched);
1778 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched);
1779 }
1780
schedule(const QString & id) const1781 MyMoneySchedule MyMoneyFile::schedule(const QString& id) const
1782 {
1783 return d->m_storage->schedule(id);
1784 }
1785
scheduleList(const QString & accountId,const Schedule::Type type,const Schedule::Occurrence occurrence,const Schedule::PaymentType paymentType,const QDate & startDate,const QDate & endDate,const bool overdue) const1786 QList<MyMoneySchedule> MyMoneyFile::scheduleList(
1787 const QString& accountId,
1788 const Schedule::Type type,
1789 const Schedule::Occurrence occurrence,
1790 const Schedule::PaymentType paymentType,
1791 const QDate& startDate,
1792 const QDate& endDate,
1793 const bool overdue) const
1794 {
1795 d->checkStorage();
1796
1797 return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue);
1798 }
1799
scheduleList(const QString & accountId) const1800 QList<MyMoneySchedule> MyMoneyFile::scheduleList(
1801 const QString& accountId) const
1802 {
1803 return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
1804 QDate(), QDate(), false);
1805 }
1806
scheduleList() const1807 QList<MyMoneySchedule> MyMoneyFile::scheduleList() const
1808 {
1809 return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
1810 QDate(), QDate(), false);
1811 }
1812
consistencyCheck()1813 QStringList MyMoneyFile::consistencyCheck()
1814 {
1815 QList<MyMoneyAccount> list;
1816 QList<MyMoneyAccount>::Iterator it_a;
1817 QList<MyMoneySchedule>::Iterator it_sch;
1818 QList<MyMoneyPayee>::Iterator it_p;
1819 QList<MyMoneyTransaction>::Iterator it_t;
1820 QList<MyMoneyReport>::Iterator it_r;
1821 QStringList accountRebuild;
1822
1823 QMap<QString, bool> interestAccounts;
1824
1825 MyMoneyAccount parent;
1826 MyMoneyAccount child;
1827 MyMoneyAccount toplevel;
1828
1829 QString parentId;
1830 QStringList rc;
1831
1832 int problemCount = 0;
1833 int unfixedCount = 0;
1834 QString problemAccount;
1835
1836 // check that we have a storage object
1837 d->checkTransaction(Q_FUNC_INFO);
1838
1839 // get the current list of accounts
1840 accountList(list);
1841 // add the standard accounts
1842 list << MyMoneyFile::instance()->asset();
1843 list << MyMoneyFile::instance()->liability();
1844 list << MyMoneyFile::instance()->income();
1845 list << MyMoneyFile::instance()->expense();
1846
1847 for (it_a = list.begin(); it_a != list.end(); ++it_a) {
1848 // no more checks for standard accounts
1849 if (isStandardAccount((*it_a).id())) {
1850 continue;
1851 }
1852
1853 switch ((*it_a).accountGroup()) {
1854 case Account::Type::Asset:
1855 toplevel = asset();
1856 break;
1857 case Account::Type::Liability:
1858 toplevel = liability();
1859 break;
1860 case Account::Type::Expense:
1861 toplevel = expense();
1862 break;
1863 case Account::Type::Income:
1864 toplevel = income();
1865 break;
1866 case Account::Type::Equity:
1867 toplevel = equity();
1868 break;
1869 default:
1870 qWarning("%s:%d This should never happen!", __FILE__ , __LINE__);
1871 break;
1872 }
1873
1874 // check for loops in the hierarchy
1875 parentId = (*it_a).parentAccountId();
1876 try {
1877 bool dropOut = false;
1878 while (!isStandardAccount(parentId) && !dropOut) {
1879 parent = account(parentId);
1880 if (parent.id() == (*it_a).id()) {
1881 // parent loops, so we need to re-parent to toplevel account
1882 // find parent account in our list
1883 problemCount++;
1884 QList<MyMoneyAccount>::Iterator it_b;
1885 for (it_b = list.begin(); it_b != list.end(); ++it_b) {
1886 if ((*it_b).id() == parent.id()) {
1887 if (problemAccount != (*it_a).name()) {
1888 problemAccount = (*it_a).name();
1889 rc << i18n("* Problem with account '%1'", problemAccount);
1890 rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name());
1891 rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name());
1892 (*it_a).setParentAccountId(toplevel.id());
1893 if (accountRebuild.contains(toplevel.id()) == 0)
1894 accountRebuild << toplevel.id();
1895 if (accountRebuild.contains((*it_a).id()) == 0)
1896 accountRebuild << (*it_a).id();
1897 dropOut = true;
1898 break;
1899 }
1900 }
1901 }
1902 }
1903 parentId = parent.parentAccountId();
1904 }
1905
1906 } catch (const MyMoneyException &) {
1907 // if we don't know about a parent, we catch it later
1908 }
1909
1910 // check that the parent exists
1911 parentId = (*it_a).parentAccountId();
1912 try {
1913 parent = account(parentId);
1914 if ((*it_a).accountGroup() != parent.accountGroup()) {
1915 problemCount++;
1916 if (problemAccount != (*it_a).name()) {
1917 problemAccount = (*it_a).name();
1918 rc << i18n("* Problem with account '%1'", problemAccount);
1919 }
1920 // the parent belongs to a different group, so we reconnect to the
1921 // master group account (asset, liability, etc) to which this account
1922 // should belong and update it in the engine.
1923 rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name());
1924 rc << i18n(" New parent account is the top level account '%1'.", toplevel.name());
1925 (*it_a).setParentAccountId(toplevel.id());
1926
1927 // make sure to rebuild the sub-accounts of the top account
1928 // and the one we removed this account from
1929 if (accountRebuild.contains(toplevel.id()) == 0)
1930 accountRebuild << toplevel.id();
1931 if (accountRebuild.contains(parent.id()) == 0)
1932 accountRebuild << parent.id();
1933 } else if (!parent.accountList().contains((*it_a).id())) {
1934 problemCount++;
1935 if (problemAccount != (*it_a).name()) {
1936 problemAccount = (*it_a).name();
1937 rc << i18n("* Problem with account '%1'", problemAccount);
1938 }
1939 // parent exists, but does not have a reference to the account
1940 rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount);
1941 if (accountRebuild.contains(parent.id()) == 0)
1942 accountRebuild << parent.id();
1943 }
1944 } catch (const MyMoneyException &) {
1945 // apparently, the parent does not exist anymore. we reconnect to the
1946 // master group account (asset, liability, etc) to which this account
1947 // should belong and update it in the engine.
1948 problemCount++;
1949 if (problemAccount != (*it_a).name()) {
1950 problemAccount = (*it_a).name();
1951 rc << i18n("* Problem with account '%1'", problemAccount);
1952 }
1953 rc << i18n(" * The parent with id %1 does not exist anymore.", parentId);
1954 rc << i18n(" New parent account is the top level account '%1'.", toplevel.name());
1955 (*it_a).setParentAccountId(toplevel.id());
1956
1957 // make sure to rebuild the sub-accounts of the top account
1958 if (accountRebuild.contains(toplevel.id()) == 0)
1959 accountRebuild << toplevel.id();
1960 }
1961
1962 // now check that all the children exist and have the correct type
1963 foreach (const auto accountID, (*it_a).accountList()) {
1964 // check that the child exists
1965 try {
1966 child = account(accountID);
1967 if (child.parentAccountId() != (*it_a).id()) {
1968 throw MYMONEYEXCEPTION_CSTRING("Child account has a different parent");
1969 }
1970 } catch (const MyMoneyException &) {
1971 problemCount++;
1972 if (problemAccount != (*it_a).name()) {
1973 problemAccount = (*it_a).name();
1974 rc << i18n("* Problem with account '%1'", problemAccount);
1975 }
1976 rc << i18n(" * Child account with id %1 does not exist anymore.", accountID);
1977 rc << i18n(" The child account list will be reconstructed.");
1978 if (accountRebuild.contains((*it_a).id()) == 0)
1979 accountRebuild << (*it_a).id();
1980 }
1981 }
1982
1983 // see if it is a loan account. if so, remember the assigned interest account
1984 if ((*it_a).isLoan()) {
1985 MyMoneyAccountLoan loan(*it_a);
1986 if (!loan.interestAccountId().isEmpty()) {
1987 interestAccounts[loan.interestAccountId()] = true;
1988 }
1989 try {
1990 payee(loan.payee());
1991 } catch (const MyMoneyException &) {
1992 problemCount++;
1993 if (problemAccount != (*it_a).name()) {
1994 problemAccount = (*it_a).name();
1995 rc << i18n("* Problem with account '%1'", problemAccount);
1996 }
1997 rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee());
1998 rc << i18n(" The payee will be removed.");
1999 // remove the payee - the account will be modified in the engine later
2000 (*it_a).deletePair("payee");
2001 }
2002 }
2003
2004 // check if it is a category and set the date to 1900-01-01 if different
2005 if ((*it_a).isIncomeExpense()) {
2006 if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) {
2007 (*it_a).setOpeningDate(QDate(1900, 1, 1));
2008 }
2009 }
2010
2011 // check for clear text online password in the online settings
2012 if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) {
2013 if (problemAccount != (*it_a).name()) {
2014 problemAccount = (*it_a).name();
2015 rc << i18n("* Problem with account '%1'", problemAccount);
2016 }
2017 rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext.");
2018 rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK.");
2019 rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version.");
2020 ++unfixedCount;
2021 }
2022
2023 // if the account was modified, we need to update it in the engine
2024 if (!(d->m_storage->account((*it_a).id()) == (*it_a))) {
2025 try {
2026 d->m_storage->modifyAccount(*it_a, true);
2027 } catch (const MyMoneyException &) {
2028 rc << i18n(" * Unable to update account data in engine.");
2029 return rc;
2030 }
2031 }
2032 }
2033
2034 if (accountRebuild.count() != 0) {
2035 rc << i18n("* Reconstructing the child lists for");
2036 }
2037
2038 // clear the affected lists
2039 for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2040 if (accountRebuild.contains((*it_a).id())) {
2041 rc << QString(" %1").arg((*it_a).name());
2042 // clear the account list
2043 (*it_a).removeAccountIds();
2044 }
2045 }
2046
2047 // reconstruct the lists
2048 for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2049 QList<MyMoneyAccount>::Iterator it;
2050 parentId = (*it_a).parentAccountId();
2051 if (accountRebuild.contains(parentId)) {
2052 for (it = list.begin(); it != list.end(); ++it) {
2053 if ((*it).id() == parentId) {
2054 (*it).addAccountId((*it_a).id());
2055 break;
2056 }
2057 }
2058 }
2059 }
2060
2061 // update the engine objects
2062 for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2063 if (accountRebuild.contains((*it_a).id())) {
2064 try {
2065 d->m_storage->modifyAccount(*it_a, true);
2066 } catch (const MyMoneyException &) {
2067 rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name());
2068 }
2069 }
2070 }
2071
2072 // For some reason, files exist with invalid ids. This has been found in the payee id
2073 // so we fix them here
2074 QList<MyMoneyPayee> pList = payeeList();
2075 QMap<QString, QString>payeeConversionMap;
2076
2077 for (it_p = pList.begin(); it_p != pList.end(); ++it_p) {
2078 if ((*it_p).id().length() > 7) {
2079 // found one of those with an invalid ids
2080 // create a new one and store it in the map.
2081 MyMoneyPayee payee = (*it_p);
2082 payee.clearId();
2083 d->m_storage->addPayee(payee);
2084 payeeConversionMap[(*it_p).id()] = payee.id();
2085 rc << i18n(" * Payee %1 recreated with fixed id", payee.name());
2086 ++problemCount;
2087 }
2088 }
2089
2090 // Fix the transactions
2091 MyMoneyTransactionFilter filter;
2092 filter.setReportAllSplits(false);
2093 const auto tList = d->m_storage->transactionList(filter);
2094 // Generate the list of interest accounts
2095 for (const auto& transaction : tList) {
2096 const auto splits = transaction.splits();
2097 for (const auto& split : splits) {
2098 if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest))
2099 interestAccounts[split.accountId()] = true;
2100 }
2101 }
2102 QSet<Account::Type> supportedAccountTypes;
2103 supportedAccountTypes << Account::Type::Checkings
2104 << Account::Type::Savings
2105 << Account::Type::Cash
2106 << Account::Type::CreditCard
2107 << Account::Type::Asset
2108 << Account::Type::Liability;
2109 QSet<QString> reportedUnsupportedAccounts;
2110
2111 for (const auto& transaction : tList) {
2112 MyMoneyTransaction t = transaction;
2113 bool tChanged = false;
2114 QDate accountOpeningDate;
2115 QStringList accountList;
2116 const auto splits = t.splits();
2117 foreach (const auto split, splits) {
2118 bool sChanged = false;
2119 MyMoneySplit s = split;
2120 if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) {
2121 s.setPayeeId(payeeConversionMap[s.payeeId()]);
2122 sChanged = true;
2123 rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id());
2124 ++problemCount;
2125 }
2126
2127 try {
2128 const auto acc = this->account(s.accountId());
2129 // compute the newest opening date of all accounts involved in the transaction
2130 // in case the newest opening date is newer than the transaction post date, do one
2131 // of the following:
2132 //
2133 // a) for category and stock accounts: update the opening date of the account
2134 // b) for account types where the user cannot modify the opening date through
2135 // the UI issue a warning (for each account only once)
2136 // c) others will be caught later
2137 if (!acc.isIncomeExpense() && !acc.isInvest()) {
2138 if (acc.openingDate() > t.postDate()) {
2139 if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) {
2140 accountOpeningDate = acc.openingDate();
2141 }
2142 accountList << this->accountToCategory(acc.id());
2143 if (!supportedAccountTypes.contains(acc.accountType())
2144 && !reportedUnsupportedAccounts.contains(acc.id())) {
2145 rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.",
2146 this->accountToCategory(acc.id()), t.id());
2147 reportedUnsupportedAccounts << acc.id();
2148 ++unfixedCount;
2149 }
2150 }
2151 } else {
2152 if (acc.openingDate() > t.postDate()) {
2153 rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.",
2154 t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate));
2155
2156 rc << i18n(" Account opening date updated.");
2157 MyMoneyAccount newAcc = acc;
2158 newAcc.setOpeningDate(t.postDate());
2159 this->modifyAccount(newAcc);
2160 ++problemCount;
2161 }
2162 }
2163
2164 // make sure, that shares and value have the same number if they
2165 // represent the same currency.
2166 if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) {
2167 // use the value as master if the transaction is balanced
2168 if (t.splitSum().isZero()) {
2169 s.setShares(s.value());
2170 rc << i18n(" * shares set to value in split of transaction '%1'.", t.id());
2171 } else {
2172 s.setValue(s.shares());
2173 rc << i18n(" * value set to shares in split of transaction '%1'.", t.id());
2174 }
2175 sChanged = true;
2176 ++problemCount;
2177 }
2178 } catch (const MyMoneyException &) {
2179 rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId());
2180 ++unfixedCount;
2181 }
2182
2183 // make sure the interest splits are marked correct as such
2184 if (interestAccounts.find(s.accountId()) != interestAccounts.end()
2185 && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
2186 s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
2187 sChanged = true;
2188 rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id());
2189 ++problemCount;
2190 }
2191
2192 if (sChanged) {
2193 tChanged = true;
2194 t.modifySplit(s);
2195 }
2196 }
2197
2198 // make sure that the transaction's post date is valid
2199 if (!t.postDate().isValid()) {
2200 tChanged = true;
2201 t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate());
2202 rc << i18n(" * Transaction '%1' has an invalid post date.", t.id());
2203 rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat));
2204 ++problemCount;
2205 }
2206 // check if the transaction's post date is after the opening date
2207 // of all accounts involved in the transaction. In case it is not,
2208 // issue a warning with the details about the transaction incl.
2209 // the account names and dates involved
2210 if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) {
2211 QDate originalPostDate = t.postDate();
2212 #if 0
2213 // for now we do not activate the logic to move the post date to a later
2214 // point in time. This could cause some severe trouble if you have lots
2215 // of ancient data collected with older versions of KMyMoney that did not
2216 // enforce certain conditions like we do now.
2217 t.setPostDate(accountOpeningDate);
2218 tChanged = true;
2219 // copy the price information for investments to the new date
2220 QList<MyMoneySplit>::const_iterator it_t;
2221 foreach (const auto split, t.splits()) {
2222 if ((split.action() != "Buy") &&
2223 (split.action() != "Reinvest")) {
2224 continue;
2225 }
2226 QString id = split.accountId();
2227 auto acc = this->account(id);
2228 MyMoneySecurity sec = this->security(acc.currencyId());
2229 MyMoneyPrice price(acc.currencyId(),
2230 sec.tradingCurrency(),
2231 t.postDate(),
2232 split.price(), "Transaction");
2233 this->addPrice(price);
2234 break;
2235 }
2236 #endif
2237 rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat));
2238 rc << i18n(" Referenced accounts: %1", accountList.join(","));
2239 rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat));
2240 ++unfixedCount;
2241 }
2242
2243 if (tChanged) {
2244 d->m_storage->modifyTransaction(t);
2245 }
2246 }
2247
2248 // Fix the schedules
2249 QList<MyMoneySchedule> schList = scheduleList();
2250 for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) {
2251 MyMoneySchedule sch = (*it_sch);
2252 MyMoneyTransaction t = sch.transaction();
2253 auto tChanged = false;
2254 foreach (const auto split, t.splits()) {
2255 MyMoneySplit s = split;
2256 bool sChanged = false;
2257 if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) {
2258 s.setPayeeId(payeeConversionMap[s.payeeId()]);
2259 sChanged = true;
2260 rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name());
2261 ++problemCount;
2262 }
2263 if (!split.value().isZero() && split.shares().isZero()) {
2264 s.setShares(s.value());
2265 sChanged = true;
2266 rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name());
2267 rc << i18n(" Shares set to value.");
2268 ++problemCount;
2269 }
2270
2271 // make sure, we don't have a bankid stored with a split in a schedule
2272 if (!split.bankID().isEmpty()) {
2273 s.setBankID(QString());
2274 sChanged = true;
2275 rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name());
2276 ++problemCount;
2277 }
2278
2279 // make sure, that shares and value have the same number if they
2280 // represent the same currency.
2281 try {
2282 const auto acc = this->account(s.accountId());
2283 if (t.commodity() == acc.currencyId()
2284 && s.shares().reduce() != s.value().reduce()) {
2285 // use the value as master if the transaction is balanced
2286 if (t.splitSum().isZero()) {
2287 s.setShares(s.value());
2288 rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name());
2289 } else {
2290 s.setValue(s.shares());
2291 rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name());
2292 }
2293 sChanged = true;
2294 ++problemCount;
2295 }
2296 } catch (const MyMoneyException &) {
2297 rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId());
2298 ++unfixedCount;
2299 }
2300 if (sChanged) {
2301 t.modifySplit(s);
2302 tChanged = true;
2303 }
2304 }
2305 if (tChanged) {
2306 sch.setTransaction(t);
2307 d->m_storage->modifySchedule(sch);
2308 }
2309 }
2310
2311 // Fix the reports
2312 QList<MyMoneyReport> rList = reportList();
2313 for (it_r = rList.begin(); it_r != rList.end(); ++it_r) {
2314 MyMoneyReport r = *it_r;
2315 QStringList payeeList;
2316 (*it_r).payees(payeeList);
2317 bool rChanged = false;
2318 for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) {
2319 if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) {
2320 rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name());
2321 ++problemCount;
2322 r.removeReference(*it_payee);
2323 r.addPayee(payeeConversionMap[*it_payee]);
2324 rChanged = true;
2325 }
2326 }
2327 if (rChanged) {
2328 d->m_storage->modifyReport(r);
2329 }
2330 }
2331
2332 // erase old payee ids
2333 QMap<QString, QString>::Iterator it_m;
2334 for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) {
2335 MyMoneyPayee payee = this->payee(it_m.key());
2336 removePayee(payee);
2337 rc << i18n(" * Payee '%1' removed.", payee.id());
2338 ++problemCount;
2339 }
2340
2341 //look for accounts which have currencies other than the base currency but no price on the opening date
2342 //all accounts using base currency are excluded, since that's the base used for foreign currency calculation
2343 //thus it is considered as always present
2344 //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their
2345 //fake opening date since a forex rate is required for all multi-currency transactions
2346
2347 //get all currencies in use
2348 QStringList currencyList;
2349 QList<MyMoneyAccount> accountForeignCurrency;
2350 QList<MyMoneyAccount> accList;
2351 accountList(accList);
2352 QList<MyMoneyAccount>::const_iterator account_it;
2353 for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) {
2354 MyMoneyAccount account = *account_it;
2355 if (!account.isIncomeExpense()
2356 && !currencyList.contains(account.currencyId())
2357 && account.currencyId() != baseCurrency().id()
2358 && !account.currencyId().isEmpty()) {
2359 //add the currency and the account-currency pair
2360 currencyList.append(account.currencyId());
2361 accountForeignCurrency.append(account);
2362 }
2363 }
2364
2365 MyMoneyPriceList pricesList = priceList();
2366 QMap<MyMoneySecurityPair, QDate> securityPriceDate;
2367
2368 //get the first date of the price for each security
2369 MyMoneyPriceList::const_iterator prices_it;
2370 for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) {
2371 MyMoneyPrice firstPrice = (*((*prices_it).constBegin()));
2372
2373 //only check the price if the currency is in use
2374 if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) {
2375 //check the security in the from field
2376 //if it is there, check if it is older
2377 QPair<QString, QString> pricePair = qMakePair(firstPrice.from(), firstPrice.to());
2378 securityPriceDate[pricePair] = firstPrice.date();
2379 }
2380 }
2381
2382 //compare the dates with the opening dates of the accounts using each currency
2383 QList<MyMoneyAccount>::const_iterator accForeignList_it;
2384 bool firstInvProblem = true;
2385 for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) {
2386 //setup the price pair correctly
2387 QPair<QString, QString> pricePair;
2388 //setup the reverse, which can also be used for rate conversion
2389 QPair<QString, QString> reversePricePair;
2390 if ((*accForeignList_it).isInvest()) {
2391 //if it is a stock, we have to search for a price from its stock to the currency of the account
2392 QString securityId = (*accForeignList_it).currencyId();
2393 QString tradingCurrencyId = security(securityId).tradingCurrency();
2394 pricePair = qMakePair(securityId, tradingCurrencyId);
2395 reversePricePair = qMakePair(tradingCurrencyId, securityId);
2396 } else {
2397 //if it is a regular account we search for a price from the currency of the account to the base currency
2398 QString currency = (*accForeignList_it).currencyId();
2399 QString baseCurrencyId = baseCurrency().id();
2400 pricePair = qMakePair(currency, baseCurrencyId);
2401 reversePricePair = qMakePair(baseCurrencyId, currency);
2402 }
2403
2404 //compare the first price with the opening date of the account
2405 if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate())
2406 && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) {
2407 if (firstInvProblem) {
2408 firstInvProblem = false;
2409 rc << i18n("* Potential problem with securities/currencies");
2410 }
2411 QDate openingDate = (*accForeignList_it).openingDate();
2412 MyMoneySecurity secError = security((*accForeignList_it).currencyId());
2413 if (!(*accForeignList_it).isInvest()) {
2414 rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate));
2415 rc << i18n(" Please enter a price for the currency on or before the opening date.");
2416 } else {
2417 rc << i18n(" * The security '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate));
2418 rc << i18n(" Please enter a price for the security on or before the opening date.");
2419 }
2420 ++unfixedCount;
2421 }
2422 }
2423
2424 // Fix the budgets that somehow still reference invalid accounts
2425 QString problemBudget;
2426 QList<MyMoneyBudget> bList = budgetList();
2427 for (QList<MyMoneyBudget>::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) {
2428 MyMoneyBudget b = *it_b;
2429 QList<MyMoneyBudget::AccountGroup> baccounts = b.getaccounts();
2430 bool bChanged = false;
2431 for (QList<MyMoneyBudget::AccountGroup>::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) {
2432 try {
2433 account((*it_bacc).id());
2434 } catch (const MyMoneyException &) {
2435 problemCount++;
2436 if (problemBudget != b.name()) {
2437 problemBudget = b.name();
2438 rc << i18n("* Problem with budget '%1'", problemBudget);
2439 }
2440 rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id());
2441 rc << i18n(" The account reference will be removed.");
2442 // remove the reference to the account
2443 b.removeReference((*it_bacc).id());
2444 bChanged = true;
2445 }
2446 }
2447 if (bChanged) {
2448 d->m_storage->modifyBudget(b);
2449 }
2450 }
2451
2452 // add more checks here
2453
2454 if (problemCount == 0 && unfixedCount == 0) {
2455 rc << i18n("Finished: data is consistent.");
2456 } else {
2457 const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount);
2458 const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount);
2459
2460 rc << QString();
2461 rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining);
2462 }
2463 return rc;
2464 }
2465
createCategory(const MyMoneyAccount & base,const QString & name)2466 QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name)
2467 {
2468 d->checkTransaction(Q_FUNC_INFO);
2469
2470 MyMoneyAccount parent = base;
2471 QString categoryText;
2472
2473 if (base.id() != expense().id() && base.id() != income().id())
2474 throw MYMONEYEXCEPTION_CSTRING("Invalid base category");
2475
2476 QStringList subAccounts = name.split(AccountSeparator);
2477 QStringList::Iterator it;
2478 for (it = subAccounts.begin(); it != subAccounts.end(); ++it) {
2479 MyMoneyAccount categoryAccount;
2480
2481 categoryAccount.setName(*it);
2482 categoryAccount.setAccountType(base.accountType());
2483
2484 if (it == subAccounts.begin())
2485 categoryText += *it;
2486 else
2487 categoryText += (AccountSeparator + *it);
2488
2489 // Only create the account if it doesn't exist
2490 try {
2491 QString categoryId = categoryToAccount(categoryText);
2492 if (categoryId.isEmpty())
2493 addAccount(categoryAccount, parent);
2494 else {
2495 categoryAccount = account(categoryId);
2496 }
2497 } catch (const MyMoneyException &e) {
2498 qDebug("Unable to add account %s, %s, %s: %s",
2499 qPrintable(categoryAccount.name()),
2500 qPrintable(parent.name()),
2501 qPrintable(categoryText),
2502 e.what());
2503 }
2504
2505 parent = categoryAccount;
2506 }
2507
2508 return categoryToAccount(name);
2509 }
2510
checkCategory(const QString & name,const MyMoneyMoney & value,const MyMoneyMoney & value2)2511 QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2)
2512 {
2513 QString accountId;
2514 MyMoneyAccount newAccount;
2515 bool found = true;
2516
2517 if (!name.isEmpty()) {
2518 // The category might be constructed with an arbitrary depth (number of
2519 // colon delimited fields). We try to find a parent account within this
2520 // hierarchy by searching the following sequence:
2521 //
2522 // aaaa:bbbb:cccc:ddddd
2523 //
2524 // 1. search aaaa:bbbb:cccc:dddd, create nothing
2525 // 2. search aaaa:bbbb:cccc , create dddd
2526 // 3. search aaaa:bbbb , create cccc:dddd
2527 // 4. search aaaa , create bbbb:cccc:dddd
2528 // 5. don't search , create aaaa:bbbb:cccc:dddd
2529
2530 newAccount.setName(name);
2531 QString accName; // part to be created (right side in above list)
2532 QString parent(name); // a possible parent part (left side in above list)
2533 do {
2534 accountId = categoryToAccount(parent);
2535 if (accountId.isEmpty()) {
2536 found = false;
2537 // prepare next step
2538 if (!accName.isEmpty())
2539 accName.prepend(':');
2540 accName.prepend(parent.section(':', -1));
2541 newAccount.setName(accName);
2542 parent = parent.section(':', 0, -2);
2543 } else if (!accName.isEmpty()) {
2544 newAccount.setParentAccountId(accountId);
2545 }
2546 } while (!parent.isEmpty() && accountId.isEmpty());
2547
2548 // if we did not find the category, we create it
2549 if (!found) {
2550 MyMoneyAccount parentAccount;
2551 if (newAccount.parentAccountId().isEmpty()) {
2552 if (!value.isNegative() && value2.isNegative())
2553 parentAccount = income();
2554 else
2555 parentAccount = expense();
2556 } else {
2557 parentAccount = account(newAccount.parentAccountId());
2558 }
2559 newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense);
2560 MyMoneyAccount brokerage;
2561 // clear out the parent id, because createAccount() does not like that
2562 newAccount.setParentAccountId(QString());
2563 createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney());
2564 accountId = newAccount.id();
2565 }
2566 }
2567
2568 return accountId;
2569 }
2570
addSecurity(MyMoneySecurity & security)2571 void MyMoneyFile::addSecurity(MyMoneySecurity& security)
2572 {
2573 d->checkTransaction(Q_FUNC_INFO);
2574
2575 d->m_storage->addSecurity(security);
2576 d->m_changeSet += MyMoneyNotification(File::Mode::Add, security);
2577 }
2578
modifySecurity(const MyMoneySecurity & security)2579 void MyMoneyFile::modifySecurity(const MyMoneySecurity& security)
2580 {
2581 d->checkTransaction(Q_FUNC_INFO);
2582
2583 d->m_storage->modifySecurity(security);
2584 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security);
2585 }
2586
removeSecurity(const MyMoneySecurity & security)2587 void MyMoneyFile::removeSecurity(const MyMoneySecurity& security)
2588 {
2589 d->checkTransaction(Q_FUNC_INFO);
2590
2591 // FIXME check that security is not referenced by other object
2592
2593 d->m_storage->removeSecurity(security);
2594 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security);
2595 }
2596
security(const QString & id) const2597 MyMoneySecurity MyMoneyFile::security(const QString& id) const
2598 {
2599 if (Q_UNLIKELY(id.isEmpty()))
2600 return baseCurrency();
2601
2602 return d->m_storage->security(id);
2603 }
2604
securityList() const2605 QList<MyMoneySecurity> MyMoneyFile::securityList() const
2606 {
2607 d->checkStorage();
2608
2609 return d->m_storage->securityList();
2610 }
2611
addCurrency(const MyMoneySecurity & currency)2612 void MyMoneyFile::addCurrency(const MyMoneySecurity& currency)
2613 {
2614 d->checkTransaction(Q_FUNC_INFO);
2615
2616 d->m_storage->addCurrency(currency);
2617 d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency);
2618 }
2619
modifyCurrency(const MyMoneySecurity & currency)2620 void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency)
2621 {
2622 d->checkTransaction(Q_FUNC_INFO);
2623
2624 // force reload of base currency object
2625 if (currency.id() == d->m_baseCurrency.id())
2626 d->m_baseCurrency.clearId();
2627
2628 d->m_storage->modifyCurrency(currency);
2629 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency);
2630 }
2631
removeCurrency(const MyMoneySecurity & currency)2632 void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency)
2633 {
2634 d->checkTransaction(Q_FUNC_INFO);
2635
2636 if (currency.id() == d->m_baseCurrency.id())
2637 throw MYMONEYEXCEPTION_CSTRING("Cannot delete base currency.");
2638
2639 // FIXME check that security is not referenced by other object
2640
2641 d->m_storage->removeCurrency(currency);
2642 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency);
2643 }
2644
currency(const QString & id) const2645 MyMoneySecurity MyMoneyFile::currency(const QString& id) const
2646 {
2647 if (id.isEmpty())
2648 return baseCurrency();
2649
2650 try {
2651 const auto currency = d->m_storage->currency(id);
2652 if (currency.id().isEmpty())
2653 throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id));
2654
2655 return currency;
2656
2657 } catch (const MyMoneyException &) {
2658 const auto security = d->m_storage->security(id);
2659 if (security.id().isEmpty()) {
2660 throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id));
2661 }
2662 return security;
2663 }
2664 }
2665
ancientCurrencies() const2666 QMap<MyMoneySecurity, MyMoneyPrice> MyMoneyFile::ancientCurrencies() const
2667 {
2668 QMap<MyMoneySecurity, MyMoneyPrice> ancientCurrencies;
2669
2670 ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney")));
2671 ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney")));
2672 ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney")));
2673 ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney")));
2674 ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney")));
2675 ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney")));
2676 ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney")));
2677 ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney")));
2678 ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney")));
2679 ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney")));
2680 ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney")));
2681 ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney")));
2682
2683 // https://en.wikipedia.org/wiki/Bulgarian_lev
2684 ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2685
2686 ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney")));
2687
2688 ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2689
2690 ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney")));
2691
2692 // Source: https://en.wikipedia.org/wiki/Turkish_lira
2693 ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney")));
2694
2695 // Source: https://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html
2696 ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney")));
2697 ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney")));
2698
2699 // Source: https://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html
2700 ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney")));
2701
2702 // Source: https://en.wikipedia.org/wiki/Mozambican_metical
2703 ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2704
2705 // Source https://en.wikipedia.org/wiki/Azerbaijani_manat
2706 ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney")));
2707
2708 // Source: https://en.wikipedia.org/wiki/Litas
2709 ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney")));
2710
2711 // Source: https://en.wikipedia.org/wiki/Belarusian_ruble
2712 ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney")));
2713
2714 // Source: https://en.wikipedia.org/wiki/Zambian_kwacha, triggered by b.k.o ticket #425530
2715 ancientCurrencies.insert(MyMoneySecurity("ZMK", i18n("Zambian Kwacha (old)"), "K"), MyMoneyPrice("ZMK", "ZMW", QDate(2013, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2716
2717 return ancientCurrencies;
2718 }
2719
availableCurrencyList() const2720 QList<MyMoneySecurity> MyMoneyFile::availableCurrencyList() const
2721 {
2722 QList<MyMoneySecurity> currencyList;
2723 currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani")));
2724 currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek")));
2725 currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder")));
2726 currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar")));
2727 currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc")));
2728 currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta")));
2729 currencyList.append(MyMoneySecurity("AOA", i18n("Angolan Kwanza"), "Kz"));
2730 currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$"));
2731 currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin")));
2732 currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$"));
2733 currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m."));
2734 currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$"));
2735 currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000));
2736 currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka")));
2737 currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$"));
2738 currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC", 100000000, 100000000));
2739 currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br"));
2740 currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$"));
2741 currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$"));
2742 currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum")));
2743 currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano")));
2744 currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark")));
2745 currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula")));
2746 currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$"));
2747 currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3)));
2748 currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$"));
2749 currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)")));
2750 currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc")));
2751 currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC")));
2752 currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO")));
2753 currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100));
2754 currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel")));
2755 currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$"));
2756 currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo")));
2757 currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$"));
2758 currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso")));
2759 currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi")));
2760 currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso")));
2761 currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc")));
2762 currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1)));
2763 currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna")));
2764 currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso")));
2765 currencyList.append(MyMoneySecurity("CUC", i18n("Cuban Convertible Peso")));
2766 currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna")));
2767 currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr"));
2768 currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc")));
2769 currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso")));
2770 currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$"));
2771 currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3)));
2772 currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon")));
2773 currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa")));
2774 currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon")));
2775 currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr")));
2776 currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac)));
2777 currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3)));
2778 currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$"));
2779 currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi")));
2780 currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari")));
2781 currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi")));
2782 currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3)));
2783 currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal")));
2784 currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso")));
2785 currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$"));
2786 currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde")));
2787 currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira")));
2788 currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$"));
2789 currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100));
2790 currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona")));
2791 currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20B9)));
2792 currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1, 0, 10));
2793 currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1));
2794 currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000));
2795 currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA)));
2796 currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$"));
2797 currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1));
2798 currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000));
2799 currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge")));
2800 currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling")));
2801 currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000));
2802 currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som")));
2803 currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD)));
2804 currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats")));
2805 currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3)));
2806 currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti")));
2807 currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$"));
2808 currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000));
2809 currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca")));
2810 currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar")));
2811 currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500));
2812 currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha")));
2813 currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit")));
2814 currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa")));
2815 currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc")));
2816 currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5));
2817 currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee")));
2818 currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$"));
2819 currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu")));
2820 currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE)));
2821 currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham")));
2822 currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT"));
2823 currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat")));
2824 currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$"));
2825 currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee")));
2826 currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$"));
2827 currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro")));
2828 currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6)));
2829 currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9)));
2830 currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr"));
2831 currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000));
2832 currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee")));
2833 currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa")));
2834 currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina")));
2835 currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani")));
2836 currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol")));
2837 currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1)));
2838 currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty")));
2839 currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial")));
2840 currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)")));
2841 currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble")));
2842 currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc")));
2843 currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala")));
2844 currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra")));
2845 currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal")));
2846 currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar")));
2847 currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee")));
2848 currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone")));
2849 currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$"));
2850 currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$"));
2851 currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling")));
2852 currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand")));
2853 currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9), 1));
2854 currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee")));
2855 currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3)));
2856 currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar")));
2857 currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder")));
2858 currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni")));
2859 currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona")));
2860 currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr"));
2861 currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3)));
2862 currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$"));
2863 currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni")));
2864 currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling")));
2865 currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F)));
2866 currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga")));
2867 currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$"));
2868 currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000));
2869 currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA)));
2870 currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat")));
2871 currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$"));
2872 currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling")));
2873 currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia")));
2874 currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo")));
2875 currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham")));
2876 currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso")));
2877 currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum")));
2878 currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu")));
2879 currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar")));
2880 currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB)));
2881 currencyList.append(MyMoneySecurity("ZMW", i18n("Zambian Kwacha"), "K"));
2882 currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$"));
2883
2884 currencyList.append(ancientCurrencies().keys());
2885
2886 // sort the currencies ...
2887 qSort(currencyList.begin(), currencyList.end(),
2888 [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2)
2889 {
2890 return c1.name().compare(c2.name()) < 0;
2891 });
2892
2893 // ... and add a few precious metals at the ned
2894 currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000));
2895 currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000));
2896 currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000));
2897 currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000));
2898
2899 return currencyList;
2900 }
2901
currencyList() const2902 QList<MyMoneySecurity> MyMoneyFile::currencyList() const
2903 {
2904 d->checkStorage();
2905
2906 return d->m_storage->currencyList();
2907 }
2908
foreignCurrency(const QString & first,const QString & second) const2909 QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const
2910 {
2911 if (baseCurrency().id() == second)
2912 return first;
2913 return second;
2914 }
2915
baseCurrency() const2916 MyMoneySecurity MyMoneyFile::baseCurrency() const
2917 {
2918 if (d->m_baseCurrency.id().isEmpty()) {
2919 QString id = QString(value("kmm-baseCurrency"));
2920 if (!id.isEmpty())
2921 d->m_baseCurrency = currency(id);
2922 }
2923
2924 return d->m_baseCurrency;
2925 }
2926
setBaseCurrency(const MyMoneySecurity & curr)2927 void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr)
2928 {
2929 // make sure the currency exists
2930 MyMoneySecurity c = currency(curr.id());
2931
2932 if (c.id() != d->m_baseCurrency.id()) {
2933 setValue("kmm-baseCurrency", curr.id());
2934 // force reload of base currency cache
2935 d->m_baseCurrency = MyMoneySecurity();
2936 }
2937 }
2938
addPrice(const MyMoneyPrice & price)2939 void MyMoneyFile::addPrice(const MyMoneyPrice& price)
2940 {
2941 if (price.rate(QString()).isZero())
2942 return;
2943
2944 d->checkTransaction(Q_FUNC_INFO);
2945
2946 // store the account's which are affected by this price regarding their value
2947 d->priceChanged(*this, price);
2948 d->m_storage->addPrice(price);
2949 }
2950
removePrice(const MyMoneyPrice & price)2951 void MyMoneyFile::removePrice(const MyMoneyPrice& price)
2952 {
2953 d->checkTransaction(Q_FUNC_INFO);
2954
2955 // store the account's which are affected by this price regarding their value
2956 d->priceChanged(*this, price);
2957 d->m_storage->removePrice(price);
2958 }
2959
price(const QString & fromId,const QString & toId,const QDate & date,const bool exactDate) const2960 MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const
2961 {
2962 d->checkStorage();
2963
2964 QString to(toId);
2965 if (to.isEmpty())
2966 to = value("kmm-baseCurrency");
2967 // if some id is missing, we can return an empty price object
2968 if (fromId.isEmpty() || to.isEmpty())
2969 return MyMoneyPrice();
2970
2971 // we don't search our tables if someone asks stupid stuff
2972 if (fromId == toId) {
2973 return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney");
2974 }
2975
2976 // if not asking for exact date, try to find the exact date match first,
2977 // either the requested price or its reciprocal value. If unsuccessful, it will move
2978 // on and look for prices of previous dates
2979 MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true);
2980 if (!rc.isValid()) {
2981 // not found, search 'to-from' rate and use reciprocal value
2982 rc = d->m_storage->price(to, fromId, date, true);
2983
2984 // not found, search previous dates, if exact date is not needed
2985 if (!exactDate && !rc.isValid()) {
2986 // search 'from-to' and 'to-from', select the most recent one
2987 MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate);
2988 MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate);
2989
2990 // check first whether both prices are valid
2991 if (fromPrice.isValid() && toPrice.isValid()) {
2992 if (fromPrice.date() >= toPrice.date()) {
2993 // if 'from-to' is newer or the same date, prefer that one
2994 rc = fromPrice;
2995 } else {
2996 // otherwise, use the reciprocal price
2997 rc = toPrice;
2998 }
2999 } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one
3000 rc = fromPrice;
3001 } else if (toPrice.isValid()) {
3002 rc = toPrice;
3003 }
3004 }
3005 }
3006 return rc;
3007 }
3008
price(const QString & fromId,const QString & toId) const3009 MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const
3010 {
3011 return price(fromId, toId, QDate::currentDate(), false);
3012 }
3013
price(const QString & fromId) const3014 MyMoneyPrice MyMoneyFile::price(const QString& fromId) const
3015 {
3016 return price(fromId, QString(), QDate::currentDate(), false);
3017 }
3018
priceList() const3019 MyMoneyPriceList MyMoneyFile::priceList() const
3020 {
3021 d->checkStorage();
3022
3023 return d->m_storage->priceList();
3024 }
3025
hasAccount(const QString & id,const QString & name) const3026 bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const
3027 {
3028 const auto accounts = account(id).accountList();
3029 for (const auto& acc : accounts) {
3030 if (account(acc).name().compare(name) == 0)
3031 return true;
3032 }
3033 return false;
3034 }
3035
reportList() const3036 QList<MyMoneyReport> MyMoneyFile::reportList() const
3037 {
3038 d->checkStorage();
3039
3040 return d->m_storage->reportList();
3041 }
3042
addReport(MyMoneyReport & report)3043 void MyMoneyFile::addReport(MyMoneyReport& report)
3044 {
3045 d->checkTransaction(Q_FUNC_INFO);
3046
3047 d->m_storage->addReport(report);
3048 }
3049
modifyReport(const MyMoneyReport & report)3050 void MyMoneyFile::modifyReport(const MyMoneyReport& report)
3051 {
3052 d->checkTransaction(Q_FUNC_INFO);
3053
3054 d->m_storage->modifyReport(report);
3055 }
3056
countReports() const3057 unsigned MyMoneyFile::countReports() const
3058 {
3059 d->checkStorage();
3060
3061 return d->m_storage->countReports();
3062 }
3063
report(const QString & id) const3064 MyMoneyReport MyMoneyFile::report(const QString& id) const
3065 {
3066 d->checkStorage();
3067
3068 return d->m_storage->report(id);
3069 }
3070
removeReport(const MyMoneyReport & report)3071 void MyMoneyFile::removeReport(const MyMoneyReport& report)
3072 {
3073 d->checkTransaction(Q_FUNC_INFO);
3074
3075 d->m_storage->removeReport(report);
3076 }
3077
3078
budgetList() const3079 QList<MyMoneyBudget> MyMoneyFile::budgetList() const
3080 {
3081 d->checkStorage();
3082
3083 return d->m_storage->budgetList();
3084 }
3085
addBudget(MyMoneyBudget & budget)3086 void MyMoneyFile::addBudget(MyMoneyBudget &budget)
3087 {
3088 d->checkTransaction(Q_FUNC_INFO);
3089
3090 d->m_storage->addBudget(budget);
3091 }
3092
budgetByName(const QString & name) const3093 MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const
3094 {
3095 d->checkStorage();
3096
3097 return d->m_storage->budgetByName(name);
3098 }
3099
modifyBudget(const MyMoneyBudget & budget)3100 void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget)
3101 {
3102 d->checkTransaction(Q_FUNC_INFO);
3103
3104 d->m_storage->modifyBudget(budget);
3105 }
3106
countBudgets() const3107 unsigned MyMoneyFile::countBudgets() const
3108 {
3109 d->checkStorage();
3110
3111 return d->m_storage->countBudgets();
3112 }
3113
budget(const QString & id) const3114 MyMoneyBudget MyMoneyFile::budget(const QString& id) const
3115 {
3116 d->checkStorage();
3117
3118 return d->m_storage->budget(id);
3119 }
3120
removeBudget(const MyMoneyBudget & budget)3121 void MyMoneyFile::removeBudget(const MyMoneyBudget& budget)
3122 {
3123 d->checkTransaction(Q_FUNC_INFO);
3124
3125 d->m_storage->removeBudget(budget);
3126 }
3127
addOnlineJob(onlineJob & job)3128 void MyMoneyFile::addOnlineJob(onlineJob& job)
3129 {
3130 d->checkTransaction(Q_FUNC_INFO);
3131
3132 d->m_storage->addOnlineJob(job);
3133 d->m_changeSet += MyMoneyNotification(File::Mode::Add, job);
3134 }
3135
modifyOnlineJob(const onlineJob job)3136 void MyMoneyFile::modifyOnlineJob(const onlineJob job)
3137 {
3138 d->checkTransaction(Q_FUNC_INFO);
3139 d->m_storage->modifyOnlineJob(job);
3140 d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job);
3141 }
3142
getOnlineJob(const QString & jobId) const3143 onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const
3144 {
3145 d->checkStorage();
3146 return d->m_storage->getOnlineJob(jobId);
3147 }
3148
onlineJobList() const3149 QList<onlineJob> MyMoneyFile::onlineJobList() const
3150 {
3151 d->checkStorage();
3152 return d->m_storage->onlineJobList();
3153 }
3154
3155 /** @todo improve speed by passing count job to m_storage */
countOnlineJobs() const3156 int MyMoneyFile::countOnlineJobs() const
3157 {
3158 return onlineJobList().count();
3159 }
3160
3161 /**
3162 * @brief Remove onlineJob
3163 * @param job onlineJob to remove
3164 */
removeOnlineJob(const onlineJob & job)3165 void MyMoneyFile::removeOnlineJob(const onlineJob& job)
3166 {
3167 d->checkTransaction(Q_FUNC_INFO);
3168
3169 // clear all changed objects from cache
3170 if (job.isLocked()) {
3171 return;
3172 }
3173 d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job);
3174 d->m_storage->removeOnlineJob(job);
3175 }
3176
removeOnlineJob(const QStringList onlineJobIds)3177 void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds)
3178 {
3179 foreach (QString jobId, onlineJobIds) {
3180 removeOnlineJob(getOnlineJob(jobId));
3181 }
3182 }
3183
costCenterList(QList<MyMoneyCostCenter> & list) const3184 void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const
3185 {
3186 d->checkStorage();
3187 list = d->m_storage->costCenterList();
3188 }
3189
updateVAT(MyMoneyTransaction & transaction) const3190 void MyMoneyFile::updateVAT(MyMoneyTransaction& transaction) const
3191 {
3192 // check if transaction qualifies
3193 const auto splitCount = transaction.splits().count();
3194 if (splitCount > 1 && splitCount <= 3) {
3195 MyMoneyMoney amount;
3196 MyMoneyAccount assetLiability;
3197 MyMoneyAccount category;
3198 MyMoneySplit taxSplit;
3199 const QString currencyId = transaction.commodity();
3200 foreach (const auto& split, transaction.splits()) {
3201 const auto acc = account(split.accountId());
3202 // all splits must reference accounts denoted in the same currency
3203 if (acc.currencyId() != currencyId) {
3204 return;
3205 }
3206 if (acc.isAssetLiability() && assetLiability.id().isEmpty()) {
3207 amount = split.shares();
3208 assetLiability = acc;
3209 continue;
3210 }
3211 if (acc.isAssetLiability()) {
3212 return;
3213 }
3214 if (category.id().isEmpty() && !acc.value("VatAccount").isEmpty()) {
3215 category = acc;
3216 continue;
3217 } else if(taxSplit.id().isEmpty() && !acc.value("Tax").toLower().compare(QLatin1String("yes"))) {
3218 taxSplit = split;
3219 continue;
3220 }
3221 return;
3222 }
3223 if (!category.id().isEmpty()) {
3224 // remove a possibly found tax split - we create a new one
3225 // but only if it is the same tax category
3226 if (!taxSplit.id().isEmpty()) {
3227 if (category.value("VatAccount").compare(taxSplit.accountId()))
3228 return;
3229 transaction.removeSplit(taxSplit);
3230 }
3231 addVATSplit(transaction, assetLiability, category, amount);
3232 }
3233 }
3234 }
3235
addVATSplit(MyMoneyTransaction & transaction,const MyMoneyAccount & acc,const MyMoneyAccount & category,const MyMoneyMoney & amount) const3236 bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& acc, const MyMoneyAccount& category, const MyMoneyMoney& amount) const
3237 {
3238 bool rc = false;
3239
3240 try {
3241 MyMoneySplit cat; // category
3242 MyMoneySplit tax; // tax
3243
3244 if (category.value("VatAccount").isEmpty())
3245 return false;
3246 MyMoneyAccount vatAcc = account(category.value("VatAccount"));
3247 const MyMoneySecurity& asec = security(acc.currencyId());
3248 const MyMoneySecurity& csec = security(category.currencyId());
3249 const MyMoneySecurity& vsec = security(vatAcc.currencyId());
3250 if (asec.id() != csec.id() || asec.id() != vsec.id()) {
3251 qDebug("Auto VAT assignment only works if all three accounts use the same currency.");
3252 return false;
3253 }
3254
3255 MyMoneyMoney vatRate(vatAcc.value("VatRate"));
3256 MyMoneyMoney gv, nv; // gross value, net value
3257 int fract = acc.fraction();
3258
3259 if (!vatRate.isZero()) {
3260
3261 tax.setAccountId(vatAcc.id());
3262
3263 // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1());
3264 if (category.value("VatAmount").toLower() != QString("net")) {
3265 // split value is the gross value
3266 gv = amount;
3267 nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract);
3268 MyMoneySplit catSplit = transaction.splitByAccount(acc.id(), false);
3269 catSplit.setShares(-nv);
3270 catSplit.setValue(catSplit.shares());
3271 transaction.modifySplit(catSplit);
3272
3273 } else {
3274 // split value is the net value
3275 nv = amount;
3276 gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract);
3277 MyMoneySplit accSplit = transaction.splitByAccount(acc.id());
3278 accSplit.setValue(gv.convert(fract));
3279 accSplit.setShares(accSplit.value());
3280 transaction.modifySplit(accSplit);
3281 }
3282
3283 tax.setValue(-(gv - nv).convert(fract));
3284 tax.setShares(tax.value());
3285 transaction.addSplit(tax);
3286 rc = true;
3287 }
3288 } catch (const MyMoneyException &) {
3289 }
3290 return rc;
3291 }
3292
isReferenced(const MyMoneyObject & obj,const QBitArray & skipChecks) const3293 bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const
3294 {
3295 d->checkStorage();
3296 return d->m_storage->isReferenced(obj, skipChecks);
3297 }
3298
isReferenced(const MyMoneyObject & obj) const3299 bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const
3300 {
3301 return isReferenced(obj, QBitArray((int)eStorage::Reference::Count));
3302 }
3303
checkNoUsed(const QString & accId,const QString & no) const3304 bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const
3305 {
3306 // by definition, an empty string or a non-numeric string is not used
3307 QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?"));
3308 if (no.isEmpty() || exp.indexIn(no) == -1)
3309 return false;
3310
3311 MyMoneyTransactionFilter filter;
3312 filter.addAccount(accId);
3313 QList<MyMoneyTransaction> transactions = transactionList(filter);
3314 QList<MyMoneyTransaction>::ConstIterator it_t = transactions.constBegin();
3315 while (it_t != transactions.constEnd()) {
3316 try {
3317 MyMoneySplit split;
3318 // Test whether the transaction also includes a split into
3319 // this account
3320 split = (*it_t).splitByAccount(accId, true /*match*/);
3321 if (!split.number().isEmpty() && split.number() == no)
3322 return true;
3323 } catch (const MyMoneyException &) {
3324 }
3325 ++it_t;
3326 }
3327 return false;
3328 }
3329
highestCheckNo(const QString & accId) const3330 QString MyMoneyFile::highestCheckNo(const QString& accId) const
3331 {
3332 unsigned64 lno = 0;
3333 unsigned64 cno;
3334 QString no;
3335 MyMoneyTransactionFilter filter;
3336 filter.addAccount(accId);
3337 QList<MyMoneyTransaction> transactions = transactionList(filter);
3338 QList<MyMoneyTransaction>::ConstIterator it_t = transactions.constBegin();
3339 while (it_t != transactions.constEnd()) {
3340 try {
3341 // Test whether the transaction also includes a split into
3342 // this account
3343 MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/);
3344 if (!split.number().isEmpty()) {
3345 // non-numerical values stored in number will return 0 in the next line
3346 cno = split.number().toULongLong();
3347 if (cno > lno) {
3348 lno = cno;
3349 no = split.number();
3350 }
3351 }
3352 } catch (const MyMoneyException &) {
3353 }
3354 ++it_t;
3355 }
3356 return no;
3357 }
3358
hasNewerTransaction(const QString & accId,const QDate & date) const3359 bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const
3360 {
3361 MyMoneyTransactionFilter filter;
3362 filter.addAccount(accId);
3363 filter.setDateFilter(date.addDays(+1), QDate());
3364 return !transactionList(filter).isEmpty();
3365 }
3366
clearCache()3367 void MyMoneyFile::clearCache()
3368 {
3369 d->checkStorage();
3370 d->m_balanceCache.clear();
3371 }
3372
forceDataChanged()3373 void MyMoneyFile::forceDataChanged()
3374 {
3375 emit dataChanged();
3376 }
3377
isTransfer(const MyMoneyTransaction & t) const3378 bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const
3379 {
3380 auto rc = true;
3381 if (t.splitCount() == 2) {
3382 foreach (const auto split, t.splits()) {
3383 auto acc = account(split.accountId());
3384 if (acc.isIncomeExpense()) {
3385 rc = false;
3386 break;
3387 }
3388 }
3389 }
3390 return rc;
3391 }
3392
referencesClosedAccount(const MyMoneyTransaction & t) const3393 bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const
3394 {
3395 auto ret = false;
3396 foreach (const auto split, t.splits()) {
3397 if (referencesClosedAccount(split)) {
3398 ret = true;
3399 break;
3400 }
3401 }
3402 return ret;
3403 }
3404
referencesClosedAccount(const MyMoneySplit & s) const3405 bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const
3406 {
3407 if (s.accountId().isEmpty())
3408 return false;
3409
3410 try {
3411 return account(s.accountId()).isClosed();
3412 } catch (const MyMoneyException &) {
3413 }
3414 return false;
3415 }
3416
storageId()3417 QString MyMoneyFile::storageId()
3418 {
3419 QString id = value("kmm-id");
3420 if (id.isEmpty()) {
3421 MyMoneyFileTransaction ft;
3422 try {
3423 QUuid uid = QUuid::createUuid();
3424 setValue("kmm-id", uid.toString());
3425 ft.commit();
3426 id = uid.toString();
3427 } catch (const MyMoneyException &) {
3428 qDebug("Unable to setup UID for new storage object");
3429 }
3430 }
3431 return id;
3432 }
3433
openingBalancesPrefix()3434 QString MyMoneyFile::openingBalancesPrefix()
3435 {
3436 return i18n("Opening Balances");
3437 }
3438
hasMatchingOnlineBalance(const MyMoneyAccount & _acc) const3439 bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const
3440 {
3441 // get current values
3442 auto acc = account(_acc.id());
3443
3444 // if there's no last transaction import data we are done
3445 if (acc.value("lastImportedTransactionDate").isEmpty()
3446 || acc.value("lastStatementBalance").isEmpty())
3447 return false;
3448
3449 // otherwise, we compare the balances
3450 MyMoneyMoney balance(acc.value("lastStatementBalance"));
3451 MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate));
3452
3453 return balance == accBalance;
3454 }
3455
countTransactionsWithSpecificReconciliationState(const QString & accId,TransactionFilter::State state) const3456 int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const
3457 {
3458 MyMoneyTransactionFilter filter;
3459 filter.addAccount(accId);
3460 filter.addState((int)state);
3461 return transactionList(filter).count();
3462 }
3463
countTransactionsWithSpecificReconciliationState() const3464 QMap<QString, QVector<int> > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const
3465 {
3466 QMap<QString, QVector<int> > result;
3467 MyMoneyTransactionFilter filter;
3468 filter.setReportAllSplits(false);
3469
3470 d->checkStorage();
3471
3472 QList<MyMoneyAccount> list;
3473 accountList(list);
3474 for (const auto& account : list) {
3475 result[account.id()] = QVector<int>((int)eMyMoney::Split::State::MaxReconcileState, 0);
3476 }
3477
3478 const auto transactions = d->m_storage->transactionList(filter);
3479 for (const auto& transaction : transactions) {
3480 const auto& splits = transaction.splits();
3481 for (const auto& split : splits) {
3482 if (!result.contains(split.accountId())) {
3483 result[split.accountId()] = QVector<int>((int)eMyMoney::Split::State::MaxReconcileState, 0);
3484 }
3485 const auto flag = split.reconcileFlag();
3486 switch(flag) {
3487 case eMyMoney::Split::State::NotReconciled:
3488 case eMyMoney::Split::State::Cleared:
3489 case eMyMoney::Split::State::Reconciled:
3490 case eMyMoney::Split::State::Frozen:
3491 result[split.accountId()][(int)flag]++;
3492 break;
3493 default:
3494 break;
3495 }
3496
3497 }
3498 }
3499 return result;
3500 }
3501
3502 /**
3503 * Make sure that the splits value has the precision of the corresponding account
3504 */
fixSplitPrecision(MyMoneyTransaction & t) const3505 void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const
3506 {
3507 auto transactionSecurity = security(t.commodity());
3508 auto transactionFraction = transactionSecurity.smallestAccountFraction();
3509
3510 for (auto& split : t.splits()) {
3511 auto acc = account(split.accountId());
3512 auto fraction = acc.fraction();
3513 if(fraction == -1) {
3514 auto sec = security(acc.currencyId());
3515 fraction = acc.fraction(sec);
3516 }
3517 // Don't do any rounding on a split factor
3518 if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) {
3519 split.setShares(static_cast<const MyMoneyMoney>(split.shares().convertDenominator(fraction).canonicalize()));
3520 split.setValue(static_cast<const MyMoneyMoney>(split.value().convertDenominator(transactionFraction).canonicalize()));
3521 }
3522 }
3523 }
3524
3525 class MyMoneyFileTransactionPrivate
3526 {
3527 Q_DISABLE_COPY(MyMoneyFileTransactionPrivate)
3528
3529 public:
MyMoneyFileTransactionPrivate()3530 MyMoneyFileTransactionPrivate() :
3531 m_isNested(MyMoneyFile::instance()->hasTransaction()),
3532 m_needRollback(!m_isNested)
3533 {
3534 }
3535
3536 public:
3537 bool m_isNested;
3538 bool m_needRollback;
3539
3540 };
3541
3542
MyMoneyFileTransaction()3543 MyMoneyFileTransaction::MyMoneyFileTransaction() :
3544 d_ptr(new MyMoneyFileTransactionPrivate)
3545 {
3546 Q_D(MyMoneyFileTransaction);
3547 if (!d->m_isNested)
3548 MyMoneyFile::instance()->startTransaction();
3549 }
3550
~MyMoneyFileTransaction()3551 MyMoneyFileTransaction::~MyMoneyFileTransaction()
3552 {
3553 try {
3554 rollback();
3555 } catch (const MyMoneyException &e) {
3556 qDebug() << e.what();
3557 }
3558 Q_D(MyMoneyFileTransaction);
3559 delete d;
3560 }
3561
restart()3562 void MyMoneyFileTransaction::restart()
3563 {
3564 rollback();
3565
3566 Q_D(MyMoneyFileTransaction);
3567 d->m_needRollback = !d->m_isNested;
3568 if (!d->m_isNested)
3569 MyMoneyFile::instance()->startTransaction();
3570 }
3571
commit()3572 void MyMoneyFileTransaction::commit()
3573 {
3574 Q_D(MyMoneyFileTransaction);
3575 if (!d->m_isNested)
3576 MyMoneyFile::instance()->commitTransaction();
3577 d->m_needRollback = false;
3578 }
3579
rollback()3580 void MyMoneyFileTransaction::rollback()
3581 {
3582 Q_D(MyMoneyFileTransaction);
3583 if (d->m_needRollback)
3584 MyMoneyFile::instance()->rollbackTransaction();
3585 d->m_needRollback = false;
3586 }
3587