File indexing completed on 2024-05-12 16:42:41

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