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 }