File indexing completed on 2024-05-12 05:06:44

0001 /*
0002     SPDX-FileCopyrightText: 2004-2006 Ace Jones <acejones@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2005-2018 Thomas Baumgart <tbaumgart@kde.org>
0004     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "mymoneystatement.h"
0009 
0010 // ----------------------------------------------------------------------------
0011 // QT Includes
0012 
0013 #include <QStringList>
0014 #include <QFile>
0015 #include <QTextStream>
0016 #include <QList>
0017 #include <QDomProcessingInstruction>
0018 #include <QDomElement>
0019 #include <QDomDocument>
0020 
0021 // ----------------------------------------------------------------------------
0022 // Project Includes
0023 
0024 namespace eMyMoney {
0025 namespace Statement {
0026 enum class Element {
0027     KMMStatement,
0028     Statement,
0029     Transaction,
0030     Split,
0031     Price,
0032     Security,
0033 };
0034 qHashSeedType qHash(const Element key, qHashSeedType seed)
0035 {
0036     return ::qHash(static_cast<uint>(key), seed);
0037 }
0038 
0039 enum class Attribute {
0040     Name,
0041     Symbol,
0042     ID,
0043     Version,
0044     AccountName,
0045     AccountNumber,
0046     BankCode,
0047     Currency,
0048     BeginDate,
0049     EndDate,
0050     ClosingBalance,
0051     Type,
0052     AccountID,
0053     SkipCategoryMatching,
0054     DatePosted,
0055     DateProcessed,
0056     Payee,
0057     Memo,
0058     Number,
0059     Amount,
0060     BankID,
0061     Reconcile,
0062     Action,
0063     Shares,
0064     Security,
0065     SecurityId,
0066     BrokerageAccount,
0067     Category,
0068     SmallestFraction,
0069 };
0070 qHashSeedType qHash(const Attribute key, qHashSeedType seed)
0071 {
0072     return ::qHash(static_cast<uint>(key), seed);
0073 }
0074 }
0075 }
0076 
0077 using namespace eMyMoney;
0078 
0079 // clang-format off
0080 static const QHash<Statement::Type, QString> txAccountType{ // clazy:exclude=non-pod-global-static
0081     {Statement::Type::None,       QStringLiteral("none")},
0082     {Statement::Type::Checkings,  QStringLiteral("checkings")},
0083     {Statement::Type::Savings,    QStringLiteral("savings")},
0084     {Statement::Type::Investment, QStringLiteral("investment")},
0085     {Statement::Type::CreditCard, QStringLiteral("creditcard")},
0086     {Statement::Type::Invalid,    QStringLiteral("invalid")},
0087 };
0088 // clang-format on
0089 
0090 // clang-format off
0091 static const QHash<Transaction::Action, QString> txAction{ // clazy:exclude=non-pod-global-static
0092     {Transaction::Action::None,             QStringLiteral("none")},
0093     {Transaction::Action::Buy,              QStringLiteral("buy")},
0094     {Transaction::Action::Sell,             QStringLiteral("sell")},
0095     {Transaction::Action::ReinvestDividend, QStringLiteral("reinvestdividend")},
0096     {Transaction::Action::CashDividend,     QStringLiteral("cashdividend")},
0097     {Transaction::Action::Shrsin,           QStringLiteral("add")},
0098     {Transaction::Action::Shrsout,          QStringLiteral("remove")},
0099     {Transaction::Action::Stksplit,         QStringLiteral("stocksplit")},
0100     {Transaction::Action::Fees,             QStringLiteral("fees")},
0101     {Transaction::Action::Interest,         QStringLiteral("interest")},
0102     {Transaction::Action::Invalid,          QStringLiteral("invalid")},
0103 };
0104 // clang-format on
0105 
0106 QString getElName(const Statement::Element el)
0107 {
0108     static const QHash<Statement::Element, QString> elNames{
0109         // clang-format off
0110         {Statement::Element::KMMStatement, QStringLiteral("KMYMONEY-STATEMENT")},
0111         {Statement::Element::Statement,    QStringLiteral("STATEMENT")},
0112         {Statement::Element::Transaction,  QStringLiteral("TRANSACTION")},
0113         {Statement::Element::Split,        QStringLiteral("SPLIT")},
0114         {Statement::Element::Price,        QStringLiteral("PRICE")},
0115         {Statement::Element::Security,     QStringLiteral("SECURITY")},
0116         // clang-format on
0117     };
0118     return elNames[el];
0119 }
0120 
0121 QString getAttrName(const Statement::Attribute attr)
0122 {
0123     static const QHash<Statement::Attribute, QString> attrNames{
0124         // clang-format off
0125         {Statement::Attribute::Name,                 QStringLiteral("name")},
0126         {Statement::Attribute::Symbol,               QStringLiteral("symbol")},
0127         {Statement::Attribute::ID,                   QStringLiteral("id")},
0128         {Statement::Attribute::Version,              QStringLiteral("version")},
0129         {Statement::Attribute::AccountName,          QStringLiteral("accountname")},
0130         {Statement::Attribute::AccountNumber,        QStringLiteral("accountnumber")},
0131         {Statement::Attribute::BankCode,             QStringLiteral("routingnumber")},
0132         {Statement::Attribute::Currency,             QStringLiteral("currency")},
0133         {Statement::Attribute::BeginDate,            QStringLiteral("begindate")},
0134         {Statement::Attribute::EndDate,              QStringLiteral("enddate")},
0135         {Statement::Attribute::ClosingBalance,       QStringLiteral("closingbalance")},
0136         {Statement::Attribute::Type,                 QStringLiteral("type")},
0137         {Statement::Attribute::AccountID,            QStringLiteral("accountid")},
0138         {Statement::Attribute::SkipCategoryMatching, QStringLiteral("skipCategoryMatching")},
0139         {Statement::Attribute::DatePosted,           QStringLiteral("dateposted")},
0140         {Statement::Attribute::DateProcessed,        QStringLiteral("entrydate")},
0141         {Statement::Attribute::Payee,                QStringLiteral("payee")},
0142         {Statement::Attribute::Memo,                 QStringLiteral("memo")},
0143         {Statement::Attribute::Number,               QStringLiteral("number")},
0144         {Statement::Attribute::Amount,               QStringLiteral("amount")},
0145         {Statement::Attribute::BankID,               QStringLiteral("bankid")},
0146         {Statement::Attribute::Reconcile,            QStringLiteral("reconcile")},
0147         {Statement::Attribute::Action,               QStringLiteral("action")},
0148         {Statement::Attribute::Shares,               QStringLiteral("shares")},
0149         {Statement::Attribute::Security,             QStringLiteral("security")},
0150         {Statement::Attribute::SecurityId,           QStringLiteral("securityId")},
0151         {Statement::Attribute::BrokerageAccount,     QStringLiteral("brokerageaccount")},
0152         {Statement::Attribute::Category,             QStringLiteral("version")},
0153         {Statement::Attribute::SmallestFraction,     QStringLiteral("smallestFraction")},
0154         // clang-format on
0155     };
0156     return attrNames[attr];
0157 }
0158 
0159 void MyMoneyStatement::write(QDomElement& _root, QDomDocument* _doc) const
0160 {
0161     QDomElement e = _doc->createElement(getElName(Statement::Element::Statement));
0162     _root.appendChild(e);
0163 
0164     e.setAttribute(getAttrName(Statement::Attribute::Version), QStringLiteral("1.1"));
0165     e.setAttribute(getAttrName(Statement::Attribute::AccountName), m_strAccountName);
0166     e.setAttribute(getAttrName(Statement::Attribute::AccountNumber), m_strAccountNumber);
0167     e.setAttribute(getAttrName(Statement::Attribute::BankCode), m_strBankCode);
0168     e.setAttribute(getAttrName(Statement::Attribute::Currency), m_strCurrency);
0169     e.setAttribute(getAttrName(Statement::Attribute::BeginDate), m_dateBegin.toString(Qt::ISODate));
0170     e.setAttribute(getAttrName(Statement::Attribute::EndDate), m_dateEnd.toString(Qt::ISODate));
0171     e.setAttribute(getAttrName(Statement::Attribute::ClosingBalance), m_closingBalance.toString());
0172     e.setAttribute(getAttrName(Statement::Attribute::Type), txAccountType[m_eType]);
0173     e.setAttribute(getAttrName(Statement::Attribute::AccountID), m_accountId);
0174     e.setAttribute(getAttrName(Statement::Attribute::SkipCategoryMatching), m_skipCategoryMatching);
0175 
0176     // iterate over transactions, and add each one
0177     for (const auto& transaction : qAsConst(m_listTransactions)) {
0178         auto p = _doc->createElement(getElName(Statement::Element::Transaction));
0179         p.setAttribute(getAttrName(Statement::Attribute::DatePosted), transaction.m_datePosted.toString(Qt::ISODate));
0180         if (transaction.m_dateProcessed.isValid()) {
0181             p.setAttribute(getAttrName(Statement::Attribute::DateProcessed), transaction.m_dateProcessed.toString(Qt::ISODate));
0182         }
0183         p.setAttribute(getAttrName(Statement::Attribute::Payee), transaction.m_strPayee);
0184         p.setAttribute(getAttrName(Statement::Attribute::Memo), transaction.m_strMemo);
0185         p.setAttribute(getAttrName(Statement::Attribute::Number), transaction.m_strNumber);
0186         p.setAttribute(getAttrName(Statement::Attribute::Amount), transaction.m_amount.toString());
0187         p.setAttribute(getAttrName(Statement::Attribute::BankID), transaction.m_strBankID);
0188         p.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)transaction.m_reconcile);
0189         p.setAttribute(getAttrName(Statement::Attribute::Action), txAction[transaction.m_eAction]);
0190 
0191         if (m_eType == eMyMoney::Statement::Type::Investment) {
0192             p.setAttribute(getAttrName(Statement::Attribute::Shares), transaction.m_shares.toString());
0193             p.setAttribute(getAttrName(Statement::Attribute::Security), transaction.m_strSecurity);
0194             p.setAttribute(getAttrName(Statement::Attribute::SecurityId), transaction.m_strSecurityId);
0195             p.setAttribute(getAttrName(Statement::Attribute::BrokerageAccount), transaction.m_strBrokerageAccount);
0196         }
0197 
0198         // add all the splits we know of (might be empty)
0199         for (const auto& split : qAsConst(transaction.m_listSplits)) {
0200             auto el = _doc->createElement(getElName(Statement::Element::Split));
0201             el.setAttribute(getAttrName(Statement::Attribute::AccountID), split.m_accountId);
0202             el.setAttribute(getAttrName(Statement::Attribute::Amount), split.m_amount.toString());
0203             el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile);
0204             el.setAttribute(getAttrName(Statement::Attribute::Category), split.m_strCategoryName);
0205             el.setAttribute(getAttrName(Statement::Attribute::Memo), split.m_strMemo);
0206             el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile);
0207             p.appendChild(el);
0208         }
0209         e.appendChild(p);
0210     }
0211 
0212     // iterate over prices, and add each one
0213     for (const auto& price : qAsConst(m_listPrices)) {
0214         auto p = _doc->createElement(getElName(Statement::Element::Price));
0215         p.setAttribute(getAttrName(Statement::Attribute::DatePosted), price.m_date.toString(Qt::ISODate));
0216         p.setAttribute(getAttrName(Statement::Attribute::Security), price.m_strSecurity);
0217         p.setAttribute(getAttrName(Statement::Attribute::Amount), price.m_amount.toString());
0218 
0219         e.appendChild(p);
0220     }
0221 
0222     // iterate over securities, and add each one
0223     for (const auto& security : qAsConst(m_listSecurities)) {
0224         auto p = _doc->createElement(getElName(Statement::Element::Security));
0225         p.setAttribute(getAttrName(Statement::Attribute::Name), security.m_strName);
0226         p.setAttribute(getAttrName(Statement::Attribute::Symbol), security.m_strSymbol);
0227         p.setAttribute(getAttrName(Statement::Attribute::ID), security.m_strId);
0228         p.setAttribute(getAttrName(Statement::Attribute::SmallestFraction), security.m_smallestFraction.toString());
0229 
0230         e.appendChild(p);
0231     }
0232 }
0233 
0234 bool MyMoneyStatement::read(const QDomElement& _e)
0235 {
0236     bool result = false;
0237 
0238     if (_e.tagName() == getElName(Statement::Element::Statement)) {
0239         result = true;
0240 
0241         m_strAccountName = _e.attribute(getAttrName(Statement::Attribute::AccountName));
0242         m_strAccountNumber = _e.attribute(getAttrName(Statement::Attribute::AccountNumber));
0243         m_strBankCode = _e.attribute(getAttrName(Statement::Attribute::BankCode));
0244         m_strCurrency = _e.attribute(getAttrName(Statement::Attribute::Currency));
0245         m_dateBegin = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::BeginDate)), Qt::ISODate);
0246         m_dateEnd = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::EndDate)), Qt::ISODate);
0247         m_closingBalance = MyMoneyMoney(_e.attribute(getAttrName(Statement::Attribute::ClosingBalance)));
0248         m_accountId = _e.attribute(getAttrName(Statement::Attribute::AccountID));
0249         m_skipCategoryMatching = _e.attribute(getAttrName(Statement::Attribute::SkipCategoryMatching)).isEmpty();
0250 
0251         auto txt = _e.attribute(getAttrName(Statement::Attribute::Type), txAccountType[Statement::Type::Checkings]);
0252         m_eType = txAccountType.key(txt, m_eType);
0253 
0254         QDomNode child = _e.firstChild();
0255         while (!child.isNull() && child.isElement()) {
0256             QDomElement c = child.toElement();
0257 
0258             if (c.tagName() == getElName(Statement::Element::Transaction)) {
0259                 MyMoneyStatement::Transaction t;
0260 
0261                 t.m_datePosted = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate);
0262                 if (c.hasAttribute(getAttrName(Statement::Attribute::DateProcessed))) {
0263                     t.m_dateProcessed = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DateProcessed)), Qt::ISODate);
0264                 }
0265                 t.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount)));
0266                 t.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo));
0267                 t.m_strNumber = c.attribute(getAttrName(Statement::Attribute::Number));
0268                 t.m_strPayee = c.attribute(getAttrName(Statement::Attribute::Payee));
0269                 t.m_strBankID = c.attribute(getAttrName(Statement::Attribute::BankID));
0270                 t.m_reconcile = static_cast<eMyMoney::Split::State>(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt());
0271 
0272                 txt = c.attribute(getAttrName(Statement::Attribute::Action), txAction[eMyMoney::Transaction::Action::Buy]);
0273                 t.m_eAction = txAction.key(txt, t.m_eAction);
0274 
0275                 if (m_eType == eMyMoney::Statement::Type::Investment) {
0276                     t.m_shares = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Shares)));
0277                     t.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security));
0278                     t.m_strSecurityId = c.attribute(getAttrName(Statement::Attribute::SecurityId));
0279                     t.m_strBrokerageAccount = c.attribute(getAttrName(Statement::Attribute::BrokerageAccount));
0280                 }
0281 
0282                 // process splits (if any)
0283                 auto splitChild = c.firstChild();
0284                 while (!splitChild.isNull() && splitChild.isElement()) {
0285                     c = splitChild.toElement();
0286                     if (c.tagName() == getElName(Statement::Element::Split)) {
0287                         MyMoneyStatement::Split s;
0288                         s.m_accountId = c.attribute(getAttrName(Statement::Attribute::AccountID));
0289                         s.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount)));
0290                         s.m_reconcile = static_cast<eMyMoney::Split::State>(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt());
0291                         s.m_strCategoryName = c.attribute(getAttrName(Statement::Attribute::Category));
0292                         s.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo));
0293                         t.m_listSplits += s;
0294                     }
0295                     splitChild = splitChild.nextSibling();
0296                 }
0297                 m_listTransactions += t;
0298             } else if (c.tagName() == getElName(Statement::Element::Price)) {
0299                 MyMoneyStatement::Price p;
0300 
0301                 p.m_date = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate);
0302                 p.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security));
0303                 p.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount)));
0304 
0305                 m_listPrices += p;
0306             } else if (c.tagName() == getElName(Statement::Element::Security)) {
0307                 MyMoneyStatement::Security s;
0308 
0309                 s.m_strName = c.attribute(getAttrName(Statement::Attribute::Name));
0310                 s.m_strSymbol = c.attribute(getAttrName(Statement::Attribute::Symbol));
0311                 s.m_strId = c.attribute(getAttrName(Statement::Attribute::ID));
0312                 s.m_smallestFraction = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::SmallestFraction)));
0313 
0314                 m_listSecurities += s;
0315             }
0316             child = child.nextSibling();
0317         }
0318     }
0319 
0320     return result;
0321 }
0322 
0323 bool MyMoneyStatement::isStatementFile(const QString& _filename)
0324 {
0325     // filename is considered a statement file if it contains
0326     // the tag "<KMYMONEY-STATEMENT>" in the first 20 lines.
0327     bool result = false;
0328 
0329     QFile f(_filename);
0330     if (f.open(QIODevice::ReadOnly)) {
0331         QTextStream ts(&f);
0332 
0333         auto lineCount = 20;
0334         while (!ts.atEnd() && !result && lineCount != 0) {
0335             if (ts.readLine().contains(QLatin1String("<KMYMONEY-STATEMENT>"), Qt::CaseInsensitive))
0336                 result = true;
0337             --lineCount;
0338         }
0339         f.close();
0340     }
0341 
0342     return result;
0343 }
0344 
0345 void MyMoneyStatement::writeXMLFile(const MyMoneyStatement& _s, const QString& _filename)
0346 {
0347     static unsigned filenum = 1;
0348     auto filename = _filename;
0349     if (filename.isEmpty()) {
0350         filename = QString::fromLatin1("statement-%1%2.xml").arg((filenum < 10) ? QStringLiteral("0") : QString()).arg(filenum);
0351         filenum++;
0352     }
0353 
0354     auto doc = new QDomDocument(getElName(Statement::Element::KMMStatement));
0355     Q_CHECK_PTR(doc);
0356 
0357     //writeStatementtoXMLDoc(_s,doc);
0358     QDomProcessingInstruction instruct = doc->createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
0359     doc->appendChild(instruct);
0360     auto eroot = doc->createElement(getElName(Statement::Element::KMMStatement));
0361     doc->appendChild(eroot);
0362     _s.write(eroot, doc);
0363 
0364     QFile g(filename);
0365     if (g.open(QIODevice::WriteOnly)) {
0366         QTextStream stream(&g);
0367 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0368         stream.setCodec("UTF-8");
0369 #endif
0370         stream << doc->toString();
0371         g.close();
0372     }
0373 
0374     delete doc;
0375 }
0376 
0377 bool MyMoneyStatement::readXMLFile(MyMoneyStatement& _s, const QString& _filename)
0378 {
0379     bool result = false;
0380     QFile f(_filename);
0381     f.open(QIODevice::ReadOnly);
0382     QDomDocument* doc = new QDomDocument;
0383     if (doc->setContent(&f, false)) {
0384         QDomElement rootElement = doc->documentElement();
0385         if (!rootElement.isNull()) {
0386             QDomNode child = rootElement.firstChild();
0387             while (!child.isNull() && child.isElement()) {
0388                 result = true;
0389                 QDomElement childElement = child.toElement();
0390                 _s.read(childElement);
0391 
0392                 child = child.nextSibling();
0393             }
0394         }
0395     }
0396     delete doc;
0397 
0398     return result;
0399 }
0400 
0401 QDate MyMoneyStatement::statementEndDate() const
0402 {
0403     if (m_dateEnd.isValid())
0404         return m_dateEnd;
0405 
0406     // if we don't have an end date listed in the
0407     // statement, we take the last postdate we
0408     // find that is not in the future and use it
0409     // as the end date of the statement.
0410     QDate postDate;
0411     for(auto& t : m_listTransactions) {
0412         if ((t.m_datePosted > postDate) && (t.m_datePosted <= QDate::currentDate())) {
0413             postDate = t.m_datePosted;
0414         }
0415     }
0416     return postDate;
0417 }