File indexing completed on 2024-04-28 16:29:37

0001 /*
0002     SPDX-FileCopyrightText: 2014 Ralf Habacker <ralf.habacker@freenet.de>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "../kmymoney/mymoney/mymoneyaccount.h"
0007 
0008 #include <QDir>
0009 #include <QFile>
0010 #include <QStringList>
0011 #include <QMap>
0012 #include <QTextStream>
0013 #include <QXmlStreamReader>
0014 #include <QDebug>
0015 
0016 #include "mymoneyenums.h"
0017 
0018 using namespace eMyMoney;
0019 
0020 QDebug operator <<(QDebug out, const QXmlStreamNamespaceDeclaration &a)
0021 {
0022     out << "QXmlStreamNamespaceDeclaration("
0023         << "prefix:" << a.prefix().toString()
0024         << "namespaceuri:" << a.namespaceUri().toString()
0025         << ")";
0026     return out;
0027 }
0028 
0029 QDebug operator <<(QDebug out, const QXmlStreamAttribute &a)
0030 {
0031     out << "QXmlStreamAttribute("
0032         << "prefix:" << a.prefix().toString()
0033         << "namespaceuri:" << a.namespaceUri().toString()
0034         << "name:" << a.name().toString()
0035         << " value:" << a.value().toString()
0036         << ")";
0037     return out;
0038 }
0039 
0040 static bool debug = false;
0041 static bool verbose = false;
0042 static bool withID = false;
0043 static bool noLevel1Names = false;
0044 static bool withTax = false;
0045 static bool prefixNameWithCode = false;
0046 
0047 typedef QMap<QString,QString> DirNameMapType;
0048 
0049 /**
0050  * map to hold differences from gnucash to kmymoney template directory
0051  * @return directory name map
0052  */
0053 DirNameMapType &getDirNameMap()
0054 {
0055     static DirNameMapType dirNameMap;
0056     dirNameMap["cs"] = "cs_CZ";
0057     dirNameMap["da"] = "dk";
0058     dirNameMap["ja"] = "ja_JP";
0059     dirNameMap["ko"] = "ko_KR";
0060     dirNameMap["nb"] = "nb_NO";
0061     dirNameMap["nl"] = "nl_NL";
0062     dirNameMap["ru"] = "ru_RU";
0063     return dirNameMap;
0064 }
0065 
0066 int toKMyMoneyAccountType(const QString& type, int index)
0067 {
0068     // in case index is 1 we process the level that represents the
0069     // KMyMoney top-level accounts. They can only have distinct values
0070     // and we have to make sure to return one of them by converting
0071     // the Account::Type into the respective accountGroup value.
0072     if (index == 1) {
0073         const auto accountType = toKMyMoneyAccountType(type, 0);
0074         if (accountType != (int)Account::Type::Unknown) {
0075             return (int)MyMoneyAccount::accountGroup(static_cast<eMyMoney::Account::Type>(accountType));
0076         }
0077         return 99; // unknown
0078     }
0079 
0080     if(type == "ROOT")              return (int)Account::Type::Unknown;
0081     else if (type == "BANK")        return (int)Account::Type::Checkings;
0082     else if (type == "CASH")        return (int)Account::Type::Cash;
0083     else if (type == "CREDIT")      return (int)Account::Type::CreditCard;
0084     else if (type == "INVEST")      return (int)Account::Type::Investment;
0085     else if (type == "RECEIVABLE")  return (int)Account::Type::Asset;
0086     else if (type == "ASSET")       return (int)Account::Type::Asset;
0087     else if (type == "PAYABLE")     return (int)Account::Type::Liability;
0088     else if (type == "LIABILITY")   return (int)Account::Type::Liability;
0089     else if (type == "CURRENCY")    return (int)Account::Type::Currency;
0090     else if (type == "INCOME")      return (int)Account::Type::Income;
0091     else if (type == "EXPENSE")     return (int)Account::Type::Expense;
0092     else if (type == "STOCK")       return (int)Account::Type::Stock;
0093     else if (type == "MUTUAL")      return (int)Account::Type::Stock;
0094     else if (type == "EQUITY")      return (int)Account::Type::Equity;
0095     else return 99; // unknown
0096 }
0097 
0098 class TemplateAccount {
0099 public:
0100     typedef QList<TemplateAccount> List;
0101     typedef QList<TemplateAccount*> PointerList;
0102     typedef QMap<QString,QString> SlotList;
0103 
0104     QString id;
0105     QString m_type;
0106     QString m_name;
0107     QString code;
0108     QString parent;
0109     SlotList slotList;
0110 
0111     TemplateAccount()
0112     {
0113     }
0114 
0115     TemplateAccount(const TemplateAccount &b)
0116         : id(b.id),
0117           m_type(b.m_type),
0118           m_name(b.m_name),
0119           code(b.code),
0120           parent(b.parent),
0121           slotList(b.slotList)
0122     {
0123     }
0124 
0125     void clear()
0126     {
0127         id.clear();
0128         m_type.clear();
0129         m_name.clear();
0130         code.clear();
0131         parent.clear();
0132         slotList.clear();
0133     }
0134 
0135     bool readSlots(QXmlStreamReader &xml)
0136     {
0137         while (!xml.atEnd()) {
0138             QXmlStreamReader::TokenType type = xml.readNext();
0139             if (type == QXmlStreamReader::StartElement) {
0140                 QStringRef _name = xml.name();
0141                 if (_name == "slot") {
0142                     type = xml.readNext();
0143                     if (type == QXmlStreamReader::Characters)
0144                         type = xml.readNext();
0145                     if (type == QXmlStreamReader::StartElement) {
0146                         QStringRef name = xml.name();
0147                         QString key, value;
0148                         if (name == "key")
0149                             key = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0150                         type = xml.readNext();
0151                         if (type == QXmlStreamReader::Characters)
0152                             type = xml.readNext();
0153                         if (type == QXmlStreamReader::StartElement) {
0154                             name = xml.name();
0155                             if (name == "value")
0156                                 value = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0157                         }
0158                         if (!key.isEmpty() && !value.isEmpty())
0159                             slotList[key] = value;
0160                     }
0161                 }
0162             } else if (type == QXmlStreamReader::EndElement) {
0163                 QStringRef _name = xml.name();
0164                 if (_name  == "slots")
0165                     return true;
0166             }
0167         }
0168         return true;
0169     }
0170 
0171     bool read(QXmlStreamReader &xml)
0172     {
0173         while (!xml.atEnd()) {
0174             xml.readNext();
0175             QStringRef _name = xml.name();
0176             if (xml.isEndElement() && _name == "account") {
0177                 if (prefixNameWithCode && !code.isEmpty() && !m_name.startsWith(code))
0178                     m_name = code + ' ' + m_name;
0179                 return true;
0180             }
0181             if (xml.isStartElement())
0182             {
0183                 if (_name == "name")
0184                     m_name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0185                 else if (_name == "id")
0186                     id = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0187                 else if (_name == "type")
0188                     m_type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0189                 else if (_name == "code")
0190                     code = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0191                 else if (_name == "parent")
0192                     parent = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
0193                 else if (_name == "slots")
0194                     readSlots(xml);
0195                 else
0196                 {
0197                     xml.readElementText(QXmlStreamReader::SkipChildElements);
0198                     if (debug)
0199                         qDebug() << "skipping" << _name.toString();
0200                 }
0201             }
0202         }
0203         return false;
0204     }
0205 };
0206 
0207 QDebug operator <<(QDebug out, const TemplateAccount &a)
0208 {
0209     out << "TemplateAccount("
0210         << "name:" << a.m_name
0211         << "id:" << a.id
0212         << "type:" << a.m_type
0213         << "code:" << a.code
0214         << "parent:" << a.parent
0215         << "slotList:" << a.slotList
0216         << ")\n";
0217     return out;
0218 }
0219 
0220 QDebug operator <<(QDebug out, const TemplateAccount::PointerList &a)
0221 {
0222     out << "TemplateAccount::List(";
0223     foreach(const TemplateAccount *account, a)
0224         out << *account;
0225     out << ")";
0226     return out;
0227 }
0228 
0229 class TemplateFile {
0230 public:
0231     QString title;
0232     QString longDescription;
0233     QString shortDescription;
0234     QString fileName;
0235     TemplateAccount::List accounts;
0236     TemplateAccount *openingBalanceAccount{nullptr};
0237 
0238     bool read(QXmlStreamReader &xml)
0239     {
0240         Q_ASSERT(xml.isStartElement() && xml.name() == "gnc-account-example");
0241 
0242         while (xml.readNextStartElement()) {
0243             QStringRef name = xml.name();
0244             if (xml.name() == "title")
0245                 title = xml.readElementText().trimmed();
0246             else if (xml.name() == "short-description")
0247                 shortDescription = xml.readElementText().trimmed().replace("  ", " ");
0248             else if (xml.name() == "long-description")
0249                 longDescription = xml.readElementText().trimmed().replace("  ", " ");
0250             else if (xml.name() == "account")
0251             {
0252                 TemplateAccount account;
0253                 if (account.read(xml))
0254                     accounts.append(account);
0255             }
0256             else
0257             {
0258                 if (debug)
0259                     qDebug() << "skipping" << name.toString();
0260                 xml.skipCurrentElement();
0261             }
0262         }
0263         return true;
0264     }
0265 
0266     bool writeAsXml(QXmlStreamWriter &xml)
0267     {
0268         xml.writeStartElement("","title");
0269         xml.writeCharacters(title);
0270         xml.writeEndElement();
0271         xml.writeStartElement("","shortdesc");
0272         xml.writeCharacters(shortDescription);
0273         xml.writeEndElement();
0274         xml.writeStartElement("","longdesc");
0275         xml.writeCharacters(longDescription);
0276         xml.writeEndElement();
0277         xml.writeStartElement("","accounts");
0278         bool result = writeAccountsAsXml(xml);
0279         xml.writeEndElement();
0280         return result;
0281     }
0282 
0283     bool writeAccountsAsXml(QXmlStreamWriter &xml, const QString &id="", int index=0)
0284     {
0285         TemplateAccount::PointerList list;
0286 
0287         if (index == 0)
0288             list = accountsByType("ROOT");
0289         else
0290             list = accountsByParentID(id);
0291 
0292         foreach(TemplateAccount *account, list)
0293         {
0294             if (account->m_type != "ROOT")
0295             {
0296                 xml.writeStartElement("","account");
0297                 xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->m_type, index)));
0298                 xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->m_name);
0299                 if (withID)
0300                     xml.writeAttribute("id", account->id);
0301                 if (withTax) {
0302                     if (account->slotList.contains("tax-related")) {
0303                         xml.writeStartElement("flag");
0304                         xml.writeAttribute("name","Tax");
0305                         xml.writeAttribute("value",account->slotList["tax-related"] == "1" ? "Yes" : "No");
0306                         xml.writeEndElement();
0307                     }
0308                 }
0309                 if (account->slotList.contains("equity-type") && account->slotList["equity-type"] == "opening-balance") {
0310                     if (openingBalanceAccount) {
0311                         qWarning() << "template" << fileName << "already has specified"
0312                                    << openingBalanceAccount->m_name
0313                                    << "as opening balance account,"
0314                                    << "ignoring account" << account->m_name;
0315                         continue;
0316                     }
0317                     xml.writeStartElement("flag");
0318                     xml.writeAttribute("name","OpeningBalanceAccount");
0319                     xml.writeAttribute("value","Yes");
0320                     xml.writeEndElement();
0321                     openingBalanceAccount = account;
0322                 }
0323             }
0324             index++;
0325             writeAccountsAsXml(xml, account->id, index);
0326             index--;
0327             xml.writeEndElement();
0328         }
0329         return true;
0330     }
0331 
0332     TemplateAccount *account(const QString &id)
0333     {
0334         for(int i=0; i < accounts.size(); i++)
0335         {
0336             TemplateAccount &account = accounts[i];
0337             if (account.id == id)
0338                 return &account;
0339         }
0340         return 0;
0341     }
0342 
0343     TemplateAccount::PointerList accountsByType(const QString &type)
0344     {
0345         TemplateAccount::PointerList list;
0346         for(int i=0; i < accounts.size(); i++)
0347         {
0348             TemplateAccount &account = accounts[i];
0349             if (account.m_type == type)
0350                 list.append(&account);
0351         }
0352         return list;
0353     }
0354 
0355 
0356     static bool nameLessThan(TemplateAccount *a1, TemplateAccount *a2)
0357     {
0358         return a1->m_name < a2->m_name;
0359     }
0360 
0361     TemplateAccount::PointerList accountsByParentID(const QString &parentID)
0362     {
0363         TemplateAccount::PointerList list;
0364 
0365         for(int i=0; i < accounts.size(); i++)
0366         {
0367             TemplateAccount &account = accounts[i];
0368             if (account.parent == parentID)
0369                 list.append(&account);
0370         }
0371         qSort(list.begin(), list.end(), nameLessThan);
0372         return list;
0373     }
0374 
0375     bool dumpTemplates(const QString &id="", int index=0)
0376     {
0377         TemplateAccount::PointerList list;
0378 
0379         if (index == 0)
0380             list = accountsByType("ROOT");
0381         else
0382             list = accountsByParentID(id);
0383 
0384         foreach(TemplateAccount *account, list)
0385         {
0386             QString a;
0387             a.fill(' ', index);
0388             qDebug() << a << account->m_name << toKMyMoneyAccountType(account->m_type, index);
0389             index++;
0390             dumpTemplates(account->id, index);
0391             index--;
0392         }
0393         return true;
0394     }
0395 };
0396 
0397 QDebug operator <<(QDebug out, const TemplateFile &a)
0398 {
0399     out << "TemplateFile("
0400         << "title:" << a.title
0401         << "short description:" << a.shortDescription
0402         << "long description:" << a.longDescription
0403         << "accounts:";
0404     foreach(const TemplateAccount &account, a.accounts)
0405         out << account;
0406     out << ")";
0407     return out;
0408 }
0409 
0410 class GnuCashAccountTemplateReader {
0411 public:
0412     GnuCashAccountTemplateReader()
0413     {
0414     }
0415 
0416     bool read(const QString &filename)
0417     {
0418         QFile file(filename);
0419         QTextStream in(&file);
0420         in.setCodec("utf-8");
0421 
0422         if(!file.open(QIODevice::ReadOnly))
0423             return false;
0424         inFileName = filename;
0425         return read(in.device());
0426     }
0427 
0428     TemplateFile &result()
0429     {
0430         return _template;
0431     }
0432 
0433     bool dumpTemplates()
0434     {
0435         return _template.dumpTemplates();
0436     }
0437 
0438     bool writeAsXml(const QString &filename=QString())
0439     {
0440         if (filename.isEmpty())
0441         {
0442             QTextStream stream(stdout);
0443             return writeAsXml(stream.device());
0444         }
0445         else
0446         {
0447             QFile file(filename);
0448             if(!file.open(QIODevice::WriteOnly))
0449                 return false;
0450             return writeAsXml(&file);
0451         }
0452     }
0453 
0454 protected:
0455 
0456     bool checkAndUpdateAvailableNamespaces(QXmlStreamReader &xml)
0457     {
0458         if (xml.namespaceDeclarations().size() < 5)
0459         {
0460             qWarning() << "gnucash template file is missing required name space declarations; adding by self";
0461         }
0462         xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("act", "http://www.gnucash.org/XML/act"));
0463         xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc", "http://www.gnucash.org/XML/gnc"));
0464         xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc-act", "http://www.gnucash.org/XML/gnc-act"));
0465         xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("cmdty","http://www.gnucash.org/XML/cmdty"));
0466         xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("slot","http://www.gnucash.org/XML/slot"));
0467         return true;
0468     }
0469 
0470     bool read(QIODevice *device)
0471     {
0472         m_xml.setDevice(device);
0473         while(!m_xml.atEnd())
0474         {
0475             m_xml.readNext();
0476             if (m_xml.isStartElement())
0477             {
0478                 if (m_xml.name() == "gnc-account-example")
0479                 {
0480                     checkAndUpdateAvailableNamespaces(m_xml);
0481                     _template.read(m_xml);
0482                 }
0483                 else
0484                     m_xml.raiseError(QObject::tr("The file is not an gnucash account template file."));
0485             }
0486         }
0487         if (m_xml.error() != QXmlStreamReader::NoError)
0488             qWarning() << m_xml.errorString();
0489         return !m_xml.error();
0490     }
0491 
0492     bool writeAsXml(QIODevice *device)
0493     {
0494         QXmlStreamWriter xml(device);
0495         xml.setAutoFormatting(true);
0496         xml.setAutoFormattingIndent(1);
0497         xml.setCodec("utf-8");
0498         xml.writeStartDocument();
0499 
0500         QString fileName = inFileName;
0501         fileName.replace(QRegExp(".*/accounts"),"accounts");
0502         xml.writeComment(QString("\n"
0503                                  "     Converted using xea2kmt from GnuCash sources\n"
0504                                  "\n"
0505                                  "        %1\n"
0506                                  "\n"
0507                                  "     Please check the source file for possible copyright\n"
0508                                  "     and license information.\n"
0509                                 ).arg(fileName));
0510         xml.writeDTD("<!DOCTYPE KMYMONEY-TEMPLATE>");
0511         xml.writeStartElement("","kmymoney-account-template");
0512         _template.fileName = fileName;
0513         bool result = _template.writeAsXml(xml);
0514         xml.writeEndElement();
0515         xml.writeEndDocument();
0516         return result;
0517     }
0518 
0519     QXmlStreamReader m_xml;
0520     TemplateFile _template;
0521     QString inFileName;
0522 };
0523 
0524 void scanDir(QDir dir, QStringList &files)
0525 {
0526     dir.setNameFilters(QStringList("*.gnucash-xea"));
0527     dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks);
0528 
0529     if (debug)
0530         qDebug() << "Scanning: " << dir.path();
0531 
0532     QStringList fileList = dir.entryList();
0533     for (int i=0; i<fileList.count(); i++)
0534     {
0535         if (debug)
0536             qDebug() << "Found file: " << fileList[i];
0537         files.append(QString("%1/%2").arg(dir.absolutePath()).arg(fileList[i]));
0538     }
0539 
0540     dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
0541     QStringList dirList = dir.entryList();
0542     for (int i=0; i<dirList.size(); ++i)
0543     {
0544         QString newPath = QString("%1/%2").arg(dir.absolutePath()).arg(dirList.at(i));
0545         scanDir(QDir(newPath), files);
0546     }
0547 }
0548 
0549 bool convertFile(const QString &inFile, const QString &outFile)
0550 {
0551     GnuCashAccountTemplateReader reader;
0552     if (!reader.read(inFile))
0553         return false;
0554     return reader.writeAsXml(outFile);
0555 }
0556 
0557 int convertFileStructure(const QString &indir, const QString &outdir)
0558 {
0559     DirNameMapType &dirNameMap = getDirNameMap();
0560     // get gnucash account files
0561     QDir d(indir);
0562     QStringList files;
0563     scanDir(d, files);
0564 
0565     QString inPath = d.absolutePath();
0566     QDir outDir(outdir);
0567     QString outPath = outDir.absolutePath();
0568     QStringList mapKeys = dirNameMap.keys();
0569     int result = 0;
0570 
0571     // process templates
0572     foreach (const QString &file, files)
0573     {
0574         if (debug || verbose)
0575             qDebug() << "processing" << file;
0576 
0577         // create output file dir
0578         QFileInfo fi(file);
0579         auto outFileName = fi.canonicalFilePath();
0580         outFileName.replace(inPath, outPath);
0581         outFileName.remove("acctchrt_");
0582         outFileName.replace(".gnucash-xea", ".kmt");
0583         foreach(const QString &key, mapKeys)
0584         {
0585             if (outFileName.contains('/' + key + '/'))
0586                 outFileName = outFileName.replace('/' + key + '/', '/' + dirNameMap[key] + '/');
0587         }
0588         fi.setFile(outFileName);
0589 
0590         d = fi.absolutePath();
0591         if (!d.exists())
0592         {
0593             if  (debug)
0594                 qDebug() << "creating path " << fi.absolutePath();
0595             d.mkpath(fi.absolutePath());
0596         }
0597         if (debug)
0598             qDebug() << "writing to " << outFileName;
0599         if (!convertFile(file, outFileName))
0600         {
0601             qWarning() << "could not create" << outFileName;
0602             result = 1;
0603         }
0604     }
0605     return result;
0606 }
0607 
0608 int main(int argc, char *argv[])
0609 {
0610     if (argc < 2 || (argc == 2 && QLatin1String(argv[1]) == "--help"))
0611     {
0612         qWarning() << "xea2kmt: convert gnucash template file to kmymoney template file";
0613         qWarning() << argv[0] << "<options> <gnucash-template-file> [<kmymoney-template-output-file>]";
0614         qWarning() << argv[0] << "<options> --in-dir <gnucash-template-files-root> --out-dir <kmymoney-template-files-root>";
0615         qWarning() << "options:";
0616         qWarning() << "          --debug                   - output debug information";
0617         qWarning() << "          --help                    - this page";
0618         qWarning() << "          --no-level1-names         - do not export account names for top level accounts";
0619         qWarning() << "          --prefix-name-with-code   - prefix account name with account code if present";
0620         qWarning() << "          --verbose                 - output processing information";
0621         qWarning() << "          --with-id                 - write account id attribute";
0622         qWarning() << "          --with-tax-related        - parse and export gnucash 'tax-related' flag";
0623         qWarning() << "          --in-dir <dir>            - search for gnucash templates files in <dir>";
0624         qWarning() << "          --out-dir <dir>           - generate kmymoney templates below <dir";
0625         return -1;
0626     }
0627 
0628     QString inFileName;
0629     QString outFileName;
0630     QString inDir;
0631     QString outDir;
0632     for(int i = 1; i < argc; i++)
0633     {
0634         QString arg = QLatin1String(argv[i]);
0635         if (arg == "--debug")
0636             debug = true;
0637         else if (arg == "--verbose")
0638             verbose = true;
0639         else if (arg == "--with-id")
0640             withID = true;
0641         else if (arg == "--no-level1-names")
0642             noLevel1Names = true;
0643         else if (arg == "--with-tax-related")
0644             withTax = true;
0645         else if (arg == "--prefix-name-with-code")
0646             prefixNameWithCode = true;
0647         else if (arg == "--in-dir")
0648             inDir = argv[++i];
0649         else if (arg == "--out-dir")
0650             outDir = argv[++i];
0651         else if (!arg.startsWith(QLatin1String("--")))
0652         {
0653             if (inFileName.isEmpty())
0654                 inFileName = arg;
0655             else
0656                 outFileName = arg;
0657         }
0658         else
0659         {
0660             qWarning() << "invalid command line parameter'" << arg << "'";
0661             return -1;
0662         }
0663     }
0664 
0665     if (!inDir.isEmpty() && !outDir.isEmpty())
0666     {
0667         return convertFileStructure(inDir, outDir);
0668     }
0669 
0670     GnuCashAccountTemplateReader reader;
0671     bool result = reader.read(inFileName);
0672     if (debug)
0673     {
0674         qDebug() << reader.result();
0675         reader.dumpTemplates();
0676     }
0677     reader.writeAsXml(outFileName);
0678     return result ? 0 : -2;
0679 }