File indexing completed on 2024-05-26 05:10:34

0001 /***************************************************************************
0002  * SPDX-FileCopyrightText: 2022 S. MANKOWSKI stephane@mankowski.fr
0003  * SPDX-FileCopyrightText: 2022 G. DE BURE support@mankowski.fr
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  ***************************************************************************/
0006 /** @file
0007  * This file is Skrooge plugin for QIF import / export.
0008  *
0009  * @author Stephane MANKOWSKI / Guillaume DE BURE
0010  */
0011 #include "skgimportpluginqif.h"
0012 
0013 #include <klocalizedstring.h>
0014 
0015 #include <kpluginfactory.h>
0016 
0017 #include <qcryptographichash.h>
0018 #include <qfile.h>
0019 #include <qsavefile.h>
0020 
0021 #include "skgbankincludes.h"
0022 #include "skgimportexportmanager.h"
0023 #include "skgservices.h"
0024 #include "skgtraces.h"
0025 
0026 /**
0027 * Opening balance string
0028  */
0029 #define OPENINGBALANCE QStringLiteral("Opening Balance")
0030 
0031 /**
0032  * This plugin factory.
0033  */
0034 K_PLUGIN_CLASS_WITH_JSON(SKGImportPluginQif, "metadata.json")
0035 
0036 SKGImportPluginQif::SKGImportPluginQif(QObject* iImporter, const QVariantList& iArg)
0037     : SKGImportPlugin(iImporter)
0038 {
0039     SKGTRACEINFUNC(10)
0040     Q_UNUSED(iArg)
0041 
0042     m_importParameters[QStringLiteral("date_format")] = QString();
0043     m_exportParameters[QStringLiteral("uuid_of_selected_accounts_or_operations")] = QString();
0044 }
0045 
0046 SKGImportPluginQif::~SKGImportPluginQif()
0047     = default;
0048 
0049 bool SKGImportPluginQif::isImportPossible()
0050 {
0051     SKGTRACEINFUNC(10)
0052     return isExportPossible();
0053 }
0054 
0055 SKGError SKGImportPluginQif::importFile()
0056 {
0057     if (m_importer->getDocument() == nullptr) {
0058         return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
0059     }
0060 
0061     SKGError err;
0062     SKGTRACEINFUNCRC(2, err)
0063 
0064     // Begin transaction
0065     err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import %1 file", "QIF"), 3);
0066     IFOK(err) {
0067         // Create account if needed
0068         QDateTime now = QDateTime::currentDateTime();
0069         QString postFix = SKGServices::dateToSqlString(now);
0070 
0071         // Step 1 done
0072         IFOKDO(err, m_importer->getDocument()->stepForward(1))
0073 
0074         // Open file
0075         QFile file(m_importer->getLocalFileName());
0076         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0077             err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message",  "Open file '%1' failed", m_importer->getFileName().toDisplayString()));
0078         } else {
0079             QTextStream stream(&file);
0080             if (!m_importer->getCodec().isEmpty()) {
0081                 stream.setCodec(m_importer->getCodec().toLatin1().constData());
0082             }
0083 
0084             // load file in memory
0085             QStringList lines;
0086             QStringList dates;
0087             bool inWrongSection = false;
0088             bool inPriceSection = false;
0089             while (!stream.atEnd()) {
0090                 // Read line
0091                 // Check line if line is empty or is a commented
0092                 QString line = stream.readLine().trimmed().toUtf8();
0093                 if (!line.isEmpty() && line[0] != '#') {
0094                     lines.push_back(line);
0095                     // Manage !Account section
0096                     if (line.startsWith(QLatin1String("!"))) {
0097                         inWrongSection = false;
0098                         inPriceSection = false;
0099                     }
0100 
0101                     if (QString::compare(line, QStringLiteral("!account"), Qt::CaseInsensitive) == 0 ||
0102                         QString::compare(line, QStringLiteral("!type:cat"), Qt::CaseInsensitive) == 0 ||
0103                         QString::compare(line, QStringLiteral("!type:tag"), Qt::CaseInsensitive) == 0 ||
0104                         QString::compare(line, QStringLiteral("!type:class"), Qt::CaseInsensitive) == 0) {
0105                         inWrongSection = true;
0106                     } else if (QString::compare(line, QStringLiteral("!type:prices"), Qt::CaseInsensitive) == 0) {
0107                         inPriceSection = true;
0108                     }
0109 
0110                     // We try to find automatically the date format
0111                     if (!inWrongSection && line[0] == 'D') {
0112                         dates.push_back(line.right(line.length() - 1));
0113                     } else if (inPriceSection) {
0114                         QStringList vals = SKGServices::splitCSVLine(line, ',');
0115                         if (vals.count() == 3) {
0116                             dates.push_back(vals.at(2));
0117                         }
0118                     }
0119                 }
0120             }
0121 
0122             // close file
0123             file.close();
0124 
0125             // Select dateformat
0126             QString dateFormat = m_importParameters.value(QStringLiteral("date_format"));
0127             if (dateFormat.isEmpty()) {
0128                 dateFormat = SKGServices::getDateFormat(dates);    // Automatic detection
0129             }
0130             if (dateFormat.isEmpty()) {
0131                 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message",  "Date format not supported"));
0132             }
0133             IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Import of '%1' with code '%2' and date format '%3'", m_importer->getFileName().toDisplayString(), m_importer->getCodec(), dateFormat)))
0134 
0135             // Step 2 done
0136             IFOKDO(err, m_importer->getDocument()->stepForward(2))
0137 
0138             // Treat all lines
0139             IFOK(err) {
0140                 SKGAccountObject* account = nullptr;
0141                 SKGOperationObject currentOperation;
0142                 SKGOperationObject payement;
0143                 SKGPayeeObject currentPayee;
0144                 SKGTrackerObject currentTracker;
0145                 SKGUnitObject currentUnit;
0146                 SKGSubOperationObject currentSubOperation;
0147                 QDate currentOperationDate;
0148                 QString lastTransferAccount;
0149                 QList<QString> transferAccount;
0150                 QList<double> transferQuantity;
0151                 bool addNextAmountToTransferQuantity = false;
0152                 QString stringForHash;
0153                 QString currentUnitForInvestment;
0154                 QChar inSection = 'B';
0155                 bool currentOperationInitialized = false;
0156                 bool latestSubCatMustBeRemoved = false;
0157                 bool investmentAccount = false;
0158                 bool div = false;
0159                 bool automaticAccount = true;
0160                 int quantityFactor = 1;
0161                 double currentUnitPrice = 1;
0162                 double checkOperationAmount = 0;
0163                 double checkSuboperationsAmount = 0;
0164                 bool openingbalancecreated = false;
0165 
0166                 int nb = lines.size();
0167                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import transactions"), nb);
0168                 for (int i = 0; !err && i < nb; ++i) {
0169                     QString line = lines.at(i);
0170                     QString val;
0171                     QChar op = line[0];
0172                     if (line.length() > 1) {
0173                         val = line.right(line.length() - 1).trimmed();
0174                     }
0175 
0176                     // Manage !Account section
0177                     if (QString::compare(line, QStringLiteral("!type:bank"), Qt::CaseInsensitive) == 0 ||
0178                         QString::compare(line, QStringLiteral("!type:cash"), Qt::CaseInsensitive) == 0 ||
0179                         QString::compare(line, QStringLiteral("!type:ccard"), Qt::CaseInsensitive) == 0 ||
0180                         QString::compare(line, QStringLiteral("!type:oth a"), Qt::CaseInsensitive) == 0 ||
0181                         QString::compare(line, QStringLiteral("!type:oth l"), Qt::CaseInsensitive) == 0 ||
0182                         QString::compare(line, QStringLiteral("!type:invst"), Qt::CaseInsensitive) == 0) {
0183                         inSection = 'B';
0184                         openingbalancecreated = false;
0185                         investmentAccount = (QString::compare(val, QStringLiteral("type:invst"), Qt::CaseInsensitive) == 0);
0186 
0187                         // Set type of account
0188                         if (account == nullptr) {
0189                             SKGAccountObject defAccount;
0190                             err = m_importer->getDefaultAccount(defAccount);
0191                             IFOKDO(err, defAccount.addOperation(currentOperation, true))
0192                             IFOK(err) account = new SKGAccountObject(defAccount);
0193                         }
0194 
0195                         if (!err && (account != nullptr)) {
0196                             err = account->setType(QString::compare(line, QStringLiteral("!type:bank"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::CURRENT :
0197                                                    (QString::compare(line, QStringLiteral("!type:ccard"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::CREDITCARD :
0198                                                     (QString::compare(line, QStringLiteral("!type:invst"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::INVESTMENT :
0199                                                      (QString::compare(line, QStringLiteral("!type:oth a"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::ASSETS : SKGAccountObject::OTHER))));
0200                             IFOKDO(err, account->save())
0201                         }
0202                     } else if (QString::compare(line, QStringLiteral("!account"), Qt::CaseInsensitive) == 0) {
0203                         inSection = 'A';
0204                         openingbalancecreated = false;
0205                         automaticAccount = false;
0206                     } else if (QString::compare(line, QStringLiteral("!type:cat"), Qt::CaseInsensitive) == 0) {
0207                         inSection = 'C';
0208                         openingbalancecreated = false;
0209                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Categories found and imported")))
0210                     } else if (QString::compare(line, QStringLiteral("!type:prices"), Qt::CaseInsensitive) == 0) {
0211                         inSection = 'U';
0212                         openingbalancecreated = false;
0213                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Units prices found and imported")))
0214                     } else if (QString::compare(line, QStringLiteral("!type:security"), Qt::CaseInsensitive) == 0) {
0215                         inSection = 'S';
0216                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Units found and imported")))
0217                     } else if (QString::compare(line, QStringLiteral("!type:tag"), Qt::CaseInsensitive) == 0) {
0218                         inSection = 'T';
0219                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Trackers found and imported")))
0220                     } else if (line.at(0) == '!') {
0221                         inSection = '?';
0222                         openingbalancecreated = false;
0223                     } else if (inSection == 'U') {
0224                         // Unit value creation
0225                         openingbalancecreated = false;
0226                         QStringList vals = SKGServices::splitCSVLine(line, ',');
0227                         if (vals.count() == 3 && !vals.at(0).isEmpty()) {
0228                             err = m_importer->getDocument()->addOrModifyUnitValue(vals.at(0), SKGServices::stringToTime(SKGServices::dateToSqlString(vals.at(2), dateFormat)).date(), SKGServices::stringToDouble(vals.at(1)));
0229                         }
0230 
0231                     } else if (inSection == 'T') {
0232                         // Tracker creation
0233                         if (op == 'N') {
0234                             IFOKDO(err, SKGTrackerObject::createTracker(m_importer->getDocument(), val, currentTracker))
0235                         } else if (op == 'D') {
0236                             IFOKDO(err, currentTracker.setComment(val))
0237                             IFOKDO(err, currentTracker.save())
0238                         }
0239                     } else if (inSection == 'S') {
0240                         // Unit creation
0241                         if (op == 'N') {
0242                             currentUnit = SKGUnitObject(m_importer->getDocument());
0243                             IFOKDO(err, currentUnit.setName(val))
0244                             IFOKDO(err, currentUnit.setSymbol(val))
0245                             IFOKDO(err, currentUnit.setType(SKGUnitObject::CURRENCY))
0246                             IFOKDO(err, currentUnit.setNumberDecimal(2))
0247                             IFOKDO(err, currentUnit.save())
0248                         } else if (op == 'S') {
0249                             IFOKDO(err, currentUnit.setSymbol(val))
0250                             IFOKDO(err, currentUnit.save())
0251                         } else if (op == 'T') {
0252                             if (QString::compare(val, QStringLiteral("stock"), Qt::CaseInsensitive) == 0) {
0253                                 IFOKDO(err, currentUnit.setType(SKGUnitObject::SHARE))
0254                                 IFOKDO(err, currentUnit.setNumberDecimal(4))
0255                                 IFOKDO(err, currentUnit.save())
0256                             }
0257                         }
0258                     } else if (inSection == 'C') {
0259                         // Category creation
0260                         openingbalancecreated = false;
0261                         if (op == 'N') {
0262                             SKGCategoryObject Category;
0263                             val.replace('/', OBJECTSEPARATOR);
0264                             val.replace(':', OBJECTSEPARATOR);
0265                             err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category);
0266                         }
0267                     } else if (inSection == 'A') {
0268                         // Account creation
0269                         openingbalancecreated = false;
0270                         if (op == 'N') {
0271                             // Check if the account already exist
0272                             SKGAccountObject account2;
0273                             err = SKGNamedObject::getObjectByName(m_importer->getDocument(), QStringLiteral("account"), val, account2);
0274                             IFKO(err) {
0275                                 // Create account
0276                                 SKGBankObject bank(m_importer->getDocument());
0277                                 err = bank.setName(i18nc("Noun",  "Bank for import %1", postFix));
0278                                 if (!err && bank.load().isFailed()) {
0279                                     err = bank.save(false);
0280                                 }
0281                                 IFOKDO(err, bank.addAccount(account2))
0282                                 IFOKDO(err, account2.setName(val))
0283                                 if (!err && account2.load().isFailed()) {
0284                                     err = account2.save(false);    // Save only
0285                                 }
0286                             }
0287 
0288                             IFOK(err) {
0289                                 delete account;
0290                                 account = new SKGAccountObject(account2);
0291                             }
0292                         } else if (op == 'D') {
0293                             if (account != nullptr) {
0294                                 err = account->setNumber(val);
0295                             }
0296                         } else if (op == 'T') {
0297                             if (account != nullptr) {
0298                                 err = account->setType(val == QStringLiteral("Bank") ? SKGAccountObject::CURRENT : (val == QStringLiteral("CCard") ? SKGAccountObject::CREDITCARD : (val == QStringLiteral("Invst") ? SKGAccountObject::INVESTMENT : (val == QStringLiteral("Oth A") ? SKGAccountObject::ASSETS : SKGAccountObject::OTHER))));
0299                             }
0300                         } else if (op == '^') {
0301                             // ^     End of entry
0302                             // save
0303                             if (account != nullptr) {
0304                                 err = account->save();
0305                             }
0306                         }
0307                     } else if (inSection == 'B') {
0308                         // Transaction creation
0309                         /*
0310                         >>>> Items for Non-Investment Accounts <<<<
0311                         DONE    D      Date
0312                         DONE    T      Amount
0313                             U      Transaction amount (higher possible value than T)
0314                         DONE    C      Cleared status
0315                         DONE    N      Number (check or reference number)
0316                         DONE    P      Payee/description
0317                         DONE    M      Memo
0318                         DONE    A      Address (up to 5 lines; 6th line is an optional message)
0319                         DONE    L      Category (category/class or transfer/class)
0320                         DONE    S      Category in split (category/class or transfer/class)
0321                         DONE    E      Memo in split
0322                         DONE    $      Dollar amount of split
0323                             %      Percentage of split if percentages are used
0324                             F      Reimbursable business expense flag
0325                             X      Small Business extensions
0326                         DONE    ^      End of entry
0327 
0328                         >>>> Items for Investment Accounts <<<<
0329                         DONE    D   Date
0330                             N   Action
0331                         DONE    Y   Security
0332                         DONE    I   Price
0333                         DONE    Q   Quantity (number of shares or split ratio)
0334                         DONE    T   Transaction amount
0335                         DONE    C   Cleared status
0336                             P   Text in the first line for transfers and reminders
0337                         DONE    M   Memo
0338                             O   Commission
0339                             L   Account for the transfer
0340                             $   Amount transferred
0341                             ^   End of entry
0342                         */
0343                         stringForHash += line;
0344                         if (op == 'D') {
0345                             // D     Date
0346                             /*
0347                             Dates in US QIF files are usually in the format MM/DD/YY, although
0348                             four-digit years are not uncommon.  Dates sometimes occur without the
0349                             slash separator, or using other separators in place of the slash,
0350                             commonly '-' and '.'.  US Quicken seems to be using the ' to indicate
0351                             post-2000 two-digit years (such as 01/01'00 for Jan 1 2000).  Some
0352                             banks appear to be using a completely undifferentiated numeric QString
0353                             formateed YYYYMMDD in downloaded QIF files.
0354                             */
0355                             // Transaction creation
0356                             SKGUnitObject unit;
0357                             IFOK(err) {
0358                                 if (account != nullptr) {
0359                                     err = account->addOperation(currentOperation, true);
0360                                     if (!openingbalancecreated) {
0361                                         double initBalance;
0362                                         account->getInitialBalance(initBalance, unit);
0363                                     }
0364                                 } else {
0365                                     SKGAccountObject defAccount;
0366                                     err = m_importer->getDefaultAccount(defAccount);
0367                                     IFOKDO(err, defAccount.addOperation(currentOperation, true))
0368                                     if (!openingbalancecreated) {
0369                                         double initBalance;
0370                                         defAccount.getInitialBalance(initBalance, unit);
0371                                     }
0372                                 }
0373                                 currentOperationInitialized = true;
0374                             }
0375 
0376                             // Set date
0377                             currentOperationDate = SKGServices::stringToTime(SKGServices::dateToSqlString(val, dateFormat)).date();
0378                             IFOKDO(err, currentOperation.setDate(currentOperationDate))
0379 
0380                             // Set unit
0381                             IFOK(err) {
0382                                 // Create unit if needed
0383                                 // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
0384                                 if (!unit.exist()) {
0385                                     err = m_importer->getDefaultUnit(unit, &currentOperationDate);
0386                                 }
0387                                 IFOKDO(err, currentOperation.setUnit(unit))
0388                             }
0389 
0390                             IFOKDO(err, currentOperation.save())
0391 
0392                             // Create suboperation
0393                             IFOKDO(err, currentOperation.addSubOperation(currentSubOperation))
0394                         } else if (op == 'Y') {
0395                             // Y     Security
0396                             if (!div) {
0397                                 currentUnitForInvestment = val;
0398 
0399                                 SKGUnitObject unit(m_importer->getDocument());
0400                                 if (currentUnitForInvestment.isEmpty()) {
0401                                     IFOKDO(err, err = m_importer->getDefaultUnit(unit))
0402                                 } else {
0403                                     IFOKDO(err, unit.setName(currentUnitForInvestment))
0404                                     IFOKDO(err, unit.setSymbol(currentUnitForInvestment))
0405                                     if (unit.load().isFailed()) {
0406                                         IFOKDO(err, unit.setType(investmentAccount ? SKGUnitObject::SHARE : SKGUnitObject::CURRENCY))
0407                                         IFOKDO(err, unit.save(false))
0408                                     }
0409                                 }
0410                                 IFOKDO(err, currentOperation.setUnit(unit))
0411                             } else {
0412                                 // For dividend, if comment is empty, we set the security in comment
0413                                 if (currentOperation.getComment().isEmpty()) {
0414                                     err = currentOperation.setComment(val);
0415                                 }
0416                             }
0417                         } else if (op == 'O') {
0418                             // O     Commission
0419                             // Get previous quantity
0420                             double quantity = SKGServices::stringToDouble(val);
0421                             SKGObjectBase::SKGListSKGObjectBase subops;
0422                             payement.getSubOperations(subops);
0423                             if (!subops.isEmpty()) {
0424                                 SKGSubOperationObject subpayement(subops.at(0));
0425                                 err = subpayement.setQuantity(subpayement.getQuantity() + quantity);
0426                                 IFOKDO(err, subpayement.save())
0427                             }
0428 
0429                             SKGSubOperationObject subcommission;
0430                             if (!payement.exist()) {
0431                                 // We have to create a new transaction
0432                                 if (account != nullptr) {
0433                                     err = account->addOperation(payement, true);
0434                                 } else {
0435                                     SKGAccountObject defAccount;
0436                                     err = m_importer->getDefaultAccount(defAccount);
0437                                     IFOKDO(err, defAccount.addOperation(payement, true))
0438                                 }
0439                                 IFOKDO(err, payement.setDate(currentOperationDate))
0440                                 IFOK(err) {
0441                                     // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
0442                                     SKGUnitObject unit;
0443                                     if ((account != nullptr) && !openingbalancecreated) {
0444                                         double initBalance;
0445                                         account->getInitialBalance(initBalance, unit);
0446                                     }
0447                                     if (!unit.exist()) {
0448                                         err = m_importer->getDefaultUnit(unit, &currentOperationDate);
0449                                     }
0450                                     IFOKDO(err, payement.setUnit(unit))
0451                                 }
0452                                 IFOKDO(err, payement.save())
0453                             }
0454                             IFOKDO(err, payement.addSubOperation(subcommission))
0455                             IFOKDO(err, subcommission.setQuantity(-quantity))
0456                             IFOKDO(err, subcommission.save(false, false))
0457                         } else if (op == 'I') {
0458                             // I     Price
0459                             currentUnitPrice = SKGServices::stringToDouble(val);
0460                             if ((currentUnitPrice != 0.0) && !currentUnitForInvestment.isEmpty()) {
0461                                 err = m_importer->getDocument()->addOrModifyUnitValue(currentUnitForInvestment, currentOperationDate, currentUnitPrice);
0462                             }
0463                         } else if (op == 'N') {
0464                             if (investmentAccount) {
0465                                 // N     Action
0466                                 /*
0467                                 QIF N Line    Notes
0468                                 ============  =====
0469                                 Aktab         Same as ShrsOut.
0470                                 AktSplit      Same as StkSplit.
0471                                 Aktzu         Same as ShrsIn.
0472                                 Buy           Buy shares.
0473                                 BuyX          Buy shares. Used with an L line.
0474                                 Cash          Miscellaneous cash transaction. Used with an L line.
0475                                 CGMid         Mid-term capital gains.
0476                                 CGMidX        Mid-term capital gains. For use with an L line.
0477                                 CGLong        Long-term capital gains.
0478                                 CGLongX       Long-term capital gains. For use with an L line.
0479                                 CGShort       Short-term capital gains.
0480                                 CGShortX      Short-term capital gains. For use with an L line.
0481                                 ContribX      Same as XIn. Used for tax-advantaged accounts.
0482                                 CvrShrt       Buy shares to cover a short sale.
0483                                 CvrShrtX      Buy shares to cover a short sale. Used with an L line.
0484                                 Div           Dividend received.
0485                                 DivX          Dividend received. For use with an L line.
0486                                 Errinerg      Same as Reminder.
0487                                 Exercise      Exercise an option.
0488                                 ExercisX      Exercise an option. For use with an L line.
0489                                 Expire        Mark an option as expired. (Uses D, N, Y & M lines)
0490                                 Grant         Receive a grant of stock options.
0491                                 Int           Same as IntInc.
0492                                 IntX          Same as IntIncX.
0493                                 IntInc        Interest received.
0494                                 IntIncX       Interest received. For use with an L line.
0495                                 K.gewsp       Same as CGShort. (German)
0496                                 K.gewspX      Same as CGShortX. (German)2307068
0497                                 Kapgew        Same as CGLong. Kapitalgewinnsteuer.(German)
0498                                 KapgewX       Same as CGLongX. Kapitalgewinnsteuer. (German)
0499                                 Kauf          Same as Buy. (German)
0500                                 KaufX         Same as BuyX. (German)
0501                                 MargInt       Margin interest paid.
0502                                 MargIntX      Margin interest paid. For use with an L line.
0503                                 MiscExp       Miscellaneous expense.
0504                                 MiscExpX      Miscellaneous expense. For use with an L line.
0505                                 MiscInc       Miscellaneous income.
0506                                 MiscIncX      Miscellaneous income. For use with an L line.
0507                                 ReinvDiv      Reinvested dividend.
0508                                 ReinvInt      Reinvested interest.
0509                                 ReinvLG       Reinvested long-term capital gains.
0510                                 Reinvkur      Same as ReinvLG.
0511                                 Reinvksp      Same as ReinvSh.
0512                                 ReinvMd       Reinvested mid-term capital gains.
0513                                 ReinvSG       Same as ReinvSh.
0514                                 ReinvSh       Reinvested short-term capital gains.
0515                                 Reinvzin      Same as ReinvDiv.
0516                                 Reminder      Reminder. (Uses D, N, C & M lines)
0517                                 RtrnCap       Return of capital.
0518                                 RtrnCapX      Return of capital. For use with an L line.
0519                                 Sell          Sell shares.
0520                                 SellX         Sell shares. For use with an L line.
0521                                 ShtSell       Short sale.
0522                                 ShrsIn        Deposit shares.
0523                                 ShrsOut       Withdraw shares.
0524                                 StkSplit      Share split.
0525                                 Verkauf       Same as Sell. (German)
0526                                 VerkaufX      Same as SellX. (German)
0527                                 Vest          Mark options as vested. (Uses N, Y, Q, C & M lines)
0528                                 WithDrwX      Same as XOut. Used for tax-advantaged accounts.
0529                                 XIn           Transfer cash from another account.
0530                                 XOut          Transfer cash to another account.
0531                                 */
0532                                 val = val.toLower();
0533                                 if (val.contains(QStringLiteral("div")) && val != QStringLiteral("reinvdiv")) {
0534                                     // TODO(Stephane MANKOWSKI) err=currentOperation.setProperty ( "SKG_OP_ORIGINAL_AMOUNT", "" );
0535                                     div = true;
0536                                 } else if (val.contains(QStringLiteral("sell")) ||
0537                                            val.contains(QStringLiteral("verkauf")) ||
0538                                            val.contains(QStringLiteral("miscexp")) ||
0539                                            val.contains(QStringLiteral("shrsout"))
0540                                           ) {
0541                                     quantityFactor = -1;
0542                                 }
0543                                 // Correction 214851 vvvv
0544                                 // err=currentOperation.setComment ( val );
0545                                 // if ( !err ) err=currentOperation.setMode ( i18nc ( "Noun, the title of an item","Title" ) );
0546                                 // Correction 214851 ^^^^
0547                             } else {
0548                                 // N     Num (check or reference number)
0549                                 // Set number
0550                                 bool ok;
0551                                 int number = val.toInt(&ok);
0552                                 if (ok && number != 0) {
0553                                     err = currentOperation.setNumber(val);
0554                                 } else {
0555                                     err = currentOperation.setMode(val);
0556                                 }
0557                             }
0558                         } else if (op == 'Q') {
0559                             // Q     Quantity (number of shares or split ratio)
0560                             // Set value
0561                             if (!val.isEmpty()) {
0562                                 double previousQuantity = currentSubOperation.getQuantity();
0563                                 if (previousQuantity != 0.0) {
0564                                     // We have to create a new transaction
0565                                     if (account != nullptr) {
0566                                         err = account->addOperation(payement, true);
0567                                     } else {
0568                                         SKGAccountObject defAccount;
0569                                         err = m_importer->getDefaultAccount(defAccount);
0570                                         IFOKDO(err, defAccount.addOperation(payement, true))
0571                                     }
0572                                     IFOKDO(err, payement.setDate(currentOperationDate))
0573                                     IFOK(err) {
0574                                         // Create unit if needed
0575                                         // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
0576                                         SKGUnitObject unit;
0577                                         if ((account != nullptr) && !openingbalancecreated) {
0578                                             double initBalance;
0579                                             account->getInitialBalance(initBalance, unit);
0580                                         }
0581                                         if (!unit.exist()) {
0582                                             err = m_importer->getDefaultUnit(unit, &currentOperationDate);
0583                                         }
0584                                         IFOKDO(err, payement.setUnit(unit))
0585                                     }
0586                                     IFOKDO(err, payement.save())
0587                                     IFOKDO(err, currentOperation.setGroupOperation(payement))
0588 
0589                                     SKGSubOperationObject subpayement;
0590                                     IFOKDO(err, payement.addSubOperation(subpayement))
0591                                     IFOKDO(err, subpayement.setQuantity(-previousQuantity))
0592                                     IFOKDO(err, subpayement.save())
0593                                 }
0594 
0595                                 IFOKDO(err, currentSubOperation.setQuantity(quantityFactor * SKGServices::stringToDouble(val)))
0596                             }
0597                         } else if (op == 'T') {
0598                             // T     Amount
0599                             // Set value
0600                             checkOperationAmount = SKGServices::stringToDouble(val);
0601                             err = currentSubOperation.setQuantity(checkOperationAmount / currentUnitPrice);
0602                             if (!err && investmentAccount) {
0603                                 err = currentOperation.setProperty(QStringLiteral("SKG_OP_ORIGINAL_AMOUNT"), val);
0604                             }
0605                         } else if (op == '$') {
0606                             // Dollar amount of split
0607                             // Set value
0608                             if (!investmentAccount) {
0609                                 double vald = SKGServices::stringToDouble(val);
0610                                 checkSuboperationsAmount += vald;
0611                                 if (addNextAmountToTransferQuantity && !lastTransferAccount.isEmpty()) {
0612                                     transferQuantity[transferAccount.count() - 1] += vald;
0613                                 }
0614                                 addNextAmountToTransferQuantity = false;
0615                                 lastTransferAccount = QString();
0616                                 err = currentSubOperation.setQuantity(vald);
0617 
0618                                 // save
0619                                 IFOKDO(err, currentSubOperation.save())
0620 
0621                                 // Create suboperation
0622                                 IFOKDO(err, currentOperation.addSubOperation(currentSubOperation))
0623 
0624                                 latestSubCatMustBeRemoved = true;
0625                             }
0626                         } else if (op == 'P') {
0627                             // P Payee
0628                             // Set Payee
0629                             // Clean QIF coming from bankperfect
0630                             val.remove(QStringLiteral("[auto]"));
0631 
0632                             err = SKGPayeeObject::createPayee(m_importer->getDocument(), val, currentPayee);
0633                             IFOKDO(err, currentOperation.setPayee(currentPayee))
0634                         } else if (op == 'A') {
0635                             // A      Address (up to 5 lines; 6th line is an optional message)
0636                             QString add = currentPayee.getAddress();
0637                             if (!add.isEmpty()) {
0638                                 add += ' ';
0639                             }
0640                             add += val;
0641                             err = currentPayee.setAddress(add);
0642                             IFOKDO(err, currentPayee.save())
0643                         } else if (op == 'M') {
0644                             // M     Memo
0645                             // Set Memo
0646                             err = currentOperation.setComment(val);
0647                         } else if (op == 'E') {
0648                             // E     Memo in split
0649                             // Set Memo
0650                             err = currentSubOperation.setComment(val);
0651                         } else if (op == 'S' || op == 'L') {
0652                             // S     Category in split (Category/Transfer/Class)
0653                             // L     Category (Category/Subcategory/Transfer/Class)
0654                             // LCategory of transaction
0655                             // L[Transfer account]
0656                             // LCategory of transaction/Class of transaction
0657                             // L[Transfer account]/Class of transaction// Set Category
0658                             if (!val.isEmpty()) {
0659                                 if (val[0] == '[') {
0660                                     addNextAmountToTransferQuantity = true;
0661 
0662                                     int pos = val.indexOf(']');
0663                                     if (pos != -1) {
0664                                         SKGPayeeObject payeeObj;
0665                                         currentOperation.getPayee(payeeObj);
0666                                         bool opening = (payeeObj.getName().compare(OPENINGBALANCE, Qt::CaseInsensitive) == 0);
0667 
0668                                         // If the very first Bank transaction in the file has a payee of "Opening Balance", the L line contains the name of the account that the file describes. This is not a transfer
0669                                         if (op == 'L' && automaticAccount && (account != nullptr) && opening) {
0670                                             QString accountName = val.mid(1, pos - 1);
0671 
0672                                             SKGAccountObject newAccount(m_importer->getDocument());
0673                                             err = newAccount.setName(accountName);
0674                                             IFOK(err) {
0675                                                 if (newAccount.exist()) {
0676                                                     // Oups, the real account is existing and it is another one
0677                                                     err = newAccount.load();
0678 
0679                                                     // We move the transaction in the right account
0680                                                     IFOKDO(err, currentOperation.setParentAccount(newAccount))
0681                                                     IFOKDO(err, currentOperation.save())
0682 
0683                                                     // We delete the previous account if empty
0684                                                     IFOK(err) {
0685                                                         if (account->getNbOperation() == 0) {
0686                                                             err = account->remove();
0687                                                         }
0688                                                         delete account;
0689                                                         account = new SKGAccountObject(newAccount);
0690                                                     }
0691                                                 } else {
0692                                                     err = account->setName(accountName);
0693                                                     IFOKDO(err, account->save())
0694                                                 }
0695                                             }
0696                                         }
0697 //                                            if ( op=='L' && currentOperation.getPayee().compare ( "Opening Balance", Qt::CaseInsensitive ) !=0 && !investmentAccount)
0698                                         if (!opening) {
0699                                             lastTransferAccount = val.mid(1, pos - 1);
0700                                             if ((account != nullptr) && lastTransferAccount == account->getName()) {
0701                                                 lastTransferAccount = QString();
0702                                             }
0703 
0704                                             if (!lastTransferAccount.isEmpty() &&
0705                                                 (transferAccount.count() == 0 ||
0706                                                  transferAccount.at(transferAccount.count() - 1) != lastTransferAccount ||
0707                                                  transferQuantity.at(transferQuantity.count() - 1) != 0.0
0708                                                 )
0709                                                ) {
0710                                                 transferAccount.append(lastTransferAccount);
0711                                                 transferQuantity.append(0.0);
0712                                             }
0713                                         }
0714                                         val = val.mid(pos + 2);
0715                                     }
0716                                 }
0717                                 if (!err && !val.isEmpty()) {
0718                                     auto cat_tag = SKGServices::splitCSVLine(val, '/', false);
0719                                     val = cat_tag.at(0);
0720                                     SKGCategoryObject Category;
0721                                     val.replace('/', OBJECTSEPARATOR);
0722                                     val.replace(':', OBJECTSEPARATOR);
0723                                     val.replace(',', OBJECTSEPARATOR);
0724                                     val.replace(';', OBJECTSEPARATOR);
0725                                     err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category);
0726                                     IFOKDO(err, currentSubOperation.setCategory(Category))
0727 
0728                                     if (!err && cat_tag.count() > 1) {
0729                                         SKGTrackerObject tracker;
0730                                         err = SKGTrackerObject::createTracker(m_importer->getDocument(), cat_tag.at(1), tracker);
0731                                         IFOKDO(err, currentSubOperation.setTracker(tracker))
0732                                     }
0733                                 }
0734                             }
0735                         } else if (op == 'C') {
0736                             // C     Cleared status
0737                             // Set status
0738                             err = currentOperation.setStatus((val == QStringLiteral("C") || val == QStringLiteral("*") ? SKGOperationObject::MARKED : (val == QStringLiteral("R") || val == QStringLiteral("X") ? SKGOperationObject::CHECKED : SKGOperationObject::NONE)));
0739                         } else if (op == '^') {
0740                             // ^     End of entry
0741                             // save
0742 
0743                             if (currentOperationInitialized) {
0744                                 QByteArray hash = QCryptographicHash::hash(stringForHash.toUtf8(), QCryptographicHash::Md5);
0745                                 SKGPayeeObject payeeObj;
0746                                 currentOperation.getPayee(payeeObj);
0747                                 bool opening = (payeeObj.getName().compare(OPENINGBALANCE, Qt::CaseInsensitive) == 0);
0748                                 if (!err && opening) {
0749                                     // Specific values for initial balance
0750                                     err = currentOperation.setStatus(SKGOperationObject::CHECKED);
0751                                     IFOKDO(err, currentOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
0752                                     IFOKDO(err, currentSubOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
0753                                     openingbalancecreated = true;
0754                                 }
0755 
0756                                 IFOKDO(err, currentOperation.setImportID(hash.toHex()))
0757                                 IFOKDO(err, currentOperation.save())
0758                                 if (!latestSubCatMustBeRemoved && !err) {
0759                                     err = currentSubOperation.save();
0760                                 }
0761 
0762                                 // Create transfers if needed
0763                                 // Get origin op
0764                                 SKGOperationObject opOrigin(m_importer->getDocument(), currentOperation.getID());
0765                                 SKGAccountObject accountOrigin;
0766                                 IFOKDO(err, opOrigin.getParentAccount(accountOrigin))
0767                                 int nbTransfers = transferAccount.count();
0768                                 for (int j = 0; !err && j < nbTransfers; ++j) {
0769                                     bool merged = false;
0770                                     double tq = transferQuantity.at(j);
0771                                     const QString& ta = transferAccount.at(j);
0772                                     // Is the transfert transaction already existing?
0773                                     double qua = tq == 0.0 && addNextAmountToTransferQuantity ? SKGServices::stringToDouble(opOrigin.getAttribute(QStringLiteral("f_QUANTITY"))) : tq;
0774                                     QString wc = "t_ACCOUNT='" % SKGServices::stringToSqlString(ta) %
0775                                                  "' AND t_TOACCOUNT='" % SKGServices::stringToSqlString(accountOrigin.getName()) %
0776                                                  "' AND ABS(f_QUANTITY-(" % SKGServices::doubleToString(-qua) % "))<0.0001"
0777                                                  " AND ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "'))<1"
0778                                                  " ORDER BY ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "')) ASC";
0779                                     SKGObjectBase::SKGListSKGObjectBase obs;
0780                                     m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display"), wc, obs);
0781                                     if (!obs.isEmpty()) {
0782                                         // We have to merge them and we do not need to create the transfer
0783                                         SKGOperationObject firstOne(obs.at(0));
0784 
0785                                         // Remove all transaction attached to this transfer
0786                                         SKGObjectBase::SKGListSKGObjectBase list;
0787                                         IFOKDO(err, firstOne.getGroupedOperations(list))
0788                                         for (const auto& o : qAsConst(list)) {
0789                                             SKGOperationObject op2(o);
0790                                             if (op2 != firstOne && op2 != currentOperation) {
0791                                                 IFOKDO(err, op2.setStatus(SKGOperationObject::NONE))
0792                                                 IFOKDO(err, op2.remove(false, true))
0793                                             }
0794                                         }
0795 
0796                                         // Attach myself
0797                                         IFOKDO(err, currentOperation.setGroupOperation(firstOne))
0798                                         IFOKDO(err, currentOperation.save())
0799 
0800                                         merged = true;
0801                                     } else {
0802                                         // Is the transaction already created as a transfer of an other one?
0803                                         QString wc = "t_import_id='QIF TRANSFER-" % SKGServices::stringToSqlString(ta) % "' AND t_ACCOUNT='" % SKGServices::stringToSqlString(accountOrigin.getName()) %
0804                                                      "' AND (ABS(f_CURRENTAMOUNT-(" % SKGServices::doubleToString(opOrigin.getCurrentAmount()) % "))<0.0001 OR f_QUANTITY=" % SKGServices::doubleToString(qua) % ")"
0805                                                      " AND ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "'))<1"
0806                                                      " ORDER BY ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "')) ASC";
0807                                         m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display"), wc, obs);
0808                                         if (!obs.isEmpty()) {
0809                                             // We have to merge them and we do not need to create the transfer
0810                                             SKGOperationObject firstOne(obs.at(0));
0811                                             err = opOrigin.setStatus(SKGOperationObject::NONE);  // To be sure we can delete it
0812                                             IFOKDO(err, opOrigin.save())
0813                                             IFOKDO(err, firstOne.mergeAttribute(opOrigin))
0814 
0815                                             SKGObjectBase::SKGListSKGObjectBase list;
0816                                             IFOKDO(err, currentOperation.getGroupedOperations(list))
0817                                             for (const auto& o : qAsConst(list)) {
0818                                                 SKGOperationObject op2(o);
0819                                                 IFOKDO(err, op2.setStatus(SKGOperationObject::NONE))
0820                                                 IFOKDO(err, op2.remove(false, true))
0821                                             }
0822                                             merged = true;
0823                                         }
0824                                     }
0825 
0826                                     if (!merged) {
0827                                         // Create target account if needed
0828                                         SKGAccountObject accountTransfer(m_importer->getDocument());
0829                                         if (m_accountCache.contains(ta)) {
0830                                             accountTransfer = m_accountCache[ta];
0831                                         } else {
0832                                             accountTransfer.setName(ta);
0833                                             if (!accountTransfer.exist()) {
0834                                                 // The account is created in the same bank by default
0835                                                 SKGBankObject bankOrigin;
0836                                                 IFOKDO(err, accountOrigin.getBank(bankOrigin))
0837                                                 IFOKDO(err, accountTransfer.setBank(bankOrigin))
0838                                                 IFOKDO(err, accountTransfer.save(false, true))
0839                                             } else {
0840                                                 err = accountTransfer.load();
0841                                             }
0842 
0843                                             m_accountCache[ta] = accountTransfer;
0844                                         }
0845 
0846                                         // Create operation
0847                                         SKGUnitObject unit;
0848                                         opOrigin.getUnit(unit);
0849 
0850                                         SKGOperationObject opTransfer;
0851                                         IFOKDO(err, accountTransfer.addOperation(opTransfer, true))
0852                                         IFOKDO(err, opTransfer.setDate(opOrigin.getDate()))
0853                                         IFOKDO(err, opTransfer.setComment(opOrigin.getComment()))
0854                                         SKGPayeeObject payeeObj2;
0855                                         opTransfer.getPayee(payeeObj2);
0856                                         IFOKDO(err, opTransfer.setPayee(payeeObj2))
0857                                         IFOKDO(err, opTransfer.setStatus(opOrigin.getStatus()))
0858                                         IFOKDO(err, opTransfer.setUnit(unit))
0859                                         IFOKDO(err, opTransfer.setImportID("QIF TRANSFER-" % accountOrigin.getName()))
0860                                         IFOKDO(err, opTransfer.save())  // save needed before setGroupOperation
0861                                         IFOKDO(err, opTransfer.setGroupOperation(opOrigin))
0862                                         IFOKDO(err, opOrigin.load())  // Must be reload because of setGroupOperation modified it
0863                                         IFOKDO(err, opTransfer.save())
0864 
0865                                         SKGSubOperationObject subopTransfer;
0866                                         IFOKDO(err, opTransfer.addSubOperation(subopTransfer))
0867                                         IFOKDO(err, subopTransfer.setQuantity(-qua))
0868                                         IFOKDO(err, subopTransfer.save())
0869                                     }
0870                                 }
0871                             }
0872 
0873                             // Check Sum($)=T for incident 214462
0874                             QString checkOperationAmountString = SKGServices::doubleToString(checkOperationAmount);
0875                             QString checkSuboperationsAmountString = SKGServices::doubleToString(checkSuboperationsAmount);
0876                             if (!err && checkOperationAmount != 0 && checkSuboperationsAmount != 0 && checkOperationAmountString != checkSuboperationsAmountString) {
0877                                 SKGSubOperationObject suboprepair;
0878                                 IFOKDO(err, currentOperation.addSubOperation(suboprepair))
0879                                 IFOKDO(err, suboprepair.setQuantity(checkOperationAmount - checkSuboperationsAmount))
0880                                 IFOKDO(err, suboprepair.setComment(i18nc("An information message",  "Auto repaired transaction")))
0881                                 IFOKDO(err, suboprepair.save())
0882 
0883                                 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "The total amount of the transaction (%1) was different to the sum of the sub-transactions (%2). The transaction has been repaired.", checkOperationAmountString, checkSuboperationsAmountString), SKGDocument::Warning))
0884                             }
0885 
0886                             // Initialize variables
0887                             currentOperationInitialized = false;
0888                             latestSubCatMustBeRemoved = false;
0889                             currentUnitForInvestment = QString();
0890                             quantityFactor = 1;
0891                             currentUnitPrice = 1;
0892                             stringForHash = QString();
0893                             checkOperationAmount = 0;
0894                             checkSuboperationsAmount = 0;
0895                             lastTransferAccount = QString();
0896                             transferAccount.clear();
0897                             transferQuantity.clear();
0898                             payement = SKGOperationObject();
0899                         } else {
0900                             // A    Address (up to five lines; the sixth line is an optional message)
0901                         }
0902                     }
0903 
0904                     if (!err && i % 500 == 0) {
0905                         err = m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE"));
0906                     }
0907                     IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
0908                 }
0909 
0910                 delete account;
0911                 account = nullptr;
0912                 SKGENDTRANSACTION(m_importer->getDocument(),  err)
0913 
0914                 // Lines treated
0915                 IFOKDO(err, m_importer->getDocument()->stepForward(3))
0916             }
0917         }
0918     }
0919     SKGENDTRANSACTION(m_importer->getDocument(),  err)
0920 
0921     return err;
0922 }
0923 
0924 bool SKGImportPluginQif::isExportPossible()
0925 {
0926     SKGTRACEINFUNC(10)
0927     return (m_importer->getDocument() == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("QIF"));
0928 }
0929 
0930 SKGError SKGImportPluginQif::exportFile()
0931 {
0932     if (m_importer->getDocument() == nullptr) {
0933         return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
0934     }
0935     SKGError err;
0936     SKGTRACEINFUNCRC(2, err)
0937 
0938     // Read parameters
0939     auto listUUIDs = SKGServices::splitCSVLine(m_exportParameters.value(QStringLiteral("uuid_of_selected_accounts_or_operations")));
0940 
0941     QStringList listOperationsToExport;
0942     listOperationsToExport.reserve(listUUIDs.count());
0943     QStringList listAccountsToExport;
0944     listAccountsToExport.reserve(listUUIDs.count());
0945     for (const auto& uuid : qAsConst(listUUIDs)) {
0946         if (uuid.endsWith(QLatin1String("-operation"))) {
0947             listOperationsToExport.push_back(uuid);
0948         } else if (uuid.endsWith(QLatin1String("-account"))) {
0949             listAccountsToExport.push_back(uuid);
0950         }
0951     }
0952 
0953     if ((listAccountsToExport.count() != 0) || (listOperationsToExport.count() != 0)) {
0954         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Only selected accounts and transactions have been exported")))
0955     }
0956 
0957     // Open file
0958     QSaveFile file(m_importer->getLocalFileName(false));
0959     if (!file.open(QIODevice::WriteOnly)) {
0960         err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message",  "Save file '%1' failed", m_importer->getFileName().toDisplayString()));
0961     } else {
0962         QTextStream stream(&file);
0963         if (!m_importer->getCodec().isEmpty()) {
0964             stream.setCodec(m_importer->getCodec().toLatin1().constData());
0965         }
0966 
0967         err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export %1 file", "QIF"), 3);
0968         IFOK(err) {
0969             // Export categories
0970             SKGObjectBase::SKGListSKGObjectBase categories;
0971             IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_category_display_tmp"), QStringLiteral("1=1 ORDER BY t_fullname, id"), categories))
0972             int nbcat = categories.count();
0973             if (!err && (nbcat != 0)) {
0974                 stream << "!Type:Cat\n";
0975                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export categories"), nbcat);
0976                 for (int i = 0; !err && i < nbcat; ++i) {
0977                     SKGCategoryObject cat(categories.at(i));
0978                     QString catName = cat.getFullName();
0979                     if (!catName.isEmpty()) {
0980                         stream << QStringLiteral("N") << catName.replace(OBJECTSEPARATOR, QStringLiteral(":")) << SKGENDL;
0981                         if (SKGServices::stringToDouble(cat.getAttribute(QStringLiteral("f_REALCURRENTAMOUNT"))) < 0) {
0982                             stream << "E" << SKGENDL;
0983                         } else {
0984                             stream << "I" << SKGENDL;
0985                         }
0986                         stream << "^" << SKGENDL;
0987                     }
0988                     IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
0989                 }
0990 
0991                 SKGENDTRANSACTION(m_importer->getDocument(),  err)
0992             }
0993             IFOKDO(err, m_importer->getDocument()->stepForward(1))
0994 
0995             SKGServices::SKGUnitInfo primaryUnit = m_importer->getDocument()->getPrimaryUnit();
0996 
0997             // Get transactions
0998             QString currentAccountName;
0999             SKGObjectBase::SKGListSKGObjectBase transactions;
1000             IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display_all"), QStringLiteral("t_template='N' ORDER BY t_ACCOUNT, d_date, id"), transactions))
1001             int nb = transactions.count();
1002             IFOK(err) {
1003                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export transactions"), nb);
1004                 for (int i = 0; !err && i < nb; ++i) {
1005                     SKGOperationObject operation(transactions.at(i));
1006                     SKGAccountObject a;
1007                     operation.getParentAccount(a);
1008                     if ((listOperationsToExport.isEmpty() || listOperationsToExport.contains(operation.getUniqueID())) &&
1009                         (listAccountsToExport.isEmpty() || listAccountsToExport.contains(a.getUniqueID()))) {
1010                         // Get account name
1011                         QString accountName = operation.getAttribute(QStringLiteral("t_ACCOUNT"));
1012 
1013                         // In the same account ?
1014                         if (accountName != currentAccountName) {
1015                             SKGAccountObject account(m_importer->getDocument());
1016                             account.setName(accountName);
1017                             account.load();
1018 
1019                             SKGBankObject bank;
1020                             account.getBank(bank);
1021 
1022                             // Write header
1023                             stream << "!Account\n";
1024                             stream << 'N' << accountName << SKGENDL;
1025                             QString type = (account.getType() == SKGAccountObject::CURRENT ? QStringLiteral("Bank") : (account.getType() == SKGAccountObject::CREDITCARD ? QStringLiteral("CCard") : (account.getType() == SKGAccountObject::INVESTMENT ? QStringLiteral("Invst") : (account.getType() == SKGAccountObject::ASSETS ? QStringLiteral("Oth A") : QStringLiteral("Cash")))));
1026                             stream << 'T' << type << SKGENDL;
1027                             QString number = bank.getNumber();
1028                             QString bnumber = account.getAgencyNumber();
1029                             QString cnumber = account.getNumber();
1030                             if (!bnumber.isEmpty()) {
1031                                 if (!number.isEmpty()) {
1032                                     number += '-';
1033                                 }
1034                                 number += bnumber;
1035                             }
1036                             if (!cnumber.isEmpty()) {
1037                                 if (!number.isEmpty()) {
1038                                     number += '-';
1039                                 }
1040                                 number += cnumber;
1041                             }
1042                             stream << 'D' << number << SKGENDL;
1043                             // stream << "/"      Statement balance date
1044                             // stream << "$"      Statement balance amount
1045                             stream << '^' << SKGENDL;
1046                             currentAccountName = accountName;
1047 
1048                             stream << "!Type:" << type << "\n";
1049                         }
1050 
1051 
1052                         // Write operation
1053                         /*
1054                         DONE    D      Date
1055                         DONE    T      Amount
1056                         N/A U      Transaction amount (higher possible value than T)
1057                         DONE    C      Cleared status
1058                         DONE    N      Number (check or reference number)
1059                         DONE    P      Payee/description
1060                         DONE    M      Memo
1061                         N/A A      Address (up to 5 lines; 6th line is an optional message)
1062                         DONE    L      Category (category/class or transfer/class)
1063                         DONE    S      Category in split (category/class or transfer/class)
1064                         DONE    E      Memo in split
1065                         DONE    $      Dollar amount of split
1066                         N/A %      Percentage of split if percentages are used
1067                         N/A F      Reimbursable business expense flag
1068                         N/A X      Small Business extensions
1069                         DONE    Y      Security
1070                         DONE    I      Price
1071                         DONE    Q      Quantity (number of shares or split ratio)
1072                         N/A    O      Commission
1073                         DONE    ^      End of entry
1074                         */
1075                         SKGUnitObject unit;
1076                         operation.getUnit(unit);
1077                         bool investment = false;
1078                         bool unitExported = false;
1079                         if (unit.getSymbol() != primaryUnit.Symbol && !primaryUnit.Symbol.isEmpty()) {
1080                             unitExported = true;
1081                         }
1082                         if (unit.getType() == SKGUnitObject::SHARE) {
1083                             unitExported = true;
1084                             investment = true;
1085                         }
1086 
1087                         QString date = SKGServices::dateToSqlString(operation.getDate());
1088                         if (date.isEmpty()) {
1089                             // This is an opening balance
1090                             date = QStringLiteral("0000-00-00");
1091                         }
1092                         stream << 'D' << date << SKGENDL;
1093                         if (!unitExported) {
1094                             stream << 'T' << SKGServices::doubleToString(operation.getCurrentAmount()) << SKGENDL;
1095                         }
1096 
1097                         if (!investment) {
1098                             auto number = operation.getNumber();
1099                             if (!number.isEmpty()) {
1100                                 stream << 'N' << operation.getNumber() << SKGENDL;
1101                             }
1102                         } else {
1103                             stream << 'N' << (operation.getCurrentAmount() > 0 ? "Buy" : "Sell") << SKGENDL;
1104                         }
1105 
1106                         if (unitExported) {
1107                             stream << 'Y' << unit.getSymbol() << SKGENDL;
1108                         }
1109 
1110                         SKGPayeeObject payeeObj;
1111                         operation.getPayee(payeeObj);
1112                         QString payee = payeeObj.getName();
1113                         QString address = payeeObj.getAddress();
1114                         if (date == QStringLiteral("0000-00-00")) {
1115                             payee = OPENINGBALANCE;
1116                         }
1117                         if (!payee.isEmpty()) {
1118                             stream << 'P' << payee << SKGENDL;
1119                         }
1120                         if (!address.isEmpty()) {
1121                             stream << 'A' << address << SKGENDL;
1122                         }
1123 
1124                         QString memo = operation.getMode() % "  " % operation.getComment();
1125                         memo = memo.trimmed();
1126                         if (!memo.isEmpty()) {
1127                             stream << 'M' << memo << SKGENDL;
1128                         }
1129 
1130                         SKGOperationObject::OperationStatus status = operation.getStatus();
1131                         stream << 'C' << (status == SKGOperationObject::MARKED ? "C" : (status == SKGOperationObject::CHECKED ? "R" : "")) << SKGENDL;
1132 
1133                         // Get sub transactions
1134                         SKGObjectBase::SKGListSKGObjectBase subtransactions;
1135                         err = operation.getSubOperations(subtransactions);
1136                         IFOK(err) {
1137                             int nbSubOps = subtransactions.size();
1138                             QString category;
1139                             if (nbSubOps == 1) {
1140                                 SKGSubOperationObject suboperation(subtransactions.at(0));
1141                                 // Dump quantity
1142                                 if (unitExported) {
1143                                     stream << 'Q' << SKGServices::doubleToString(qAbs(suboperation.getQuantity())) << SKGENDL;
1144                                     stream << 'I' << SKGServices::doubleToString(qAbs(operation.getCurrentAmount() / suboperation.getQuantity())) << SKGENDL;
1145                                 }
1146 
1147                                 // Get category of this simple operation
1148                                 SKGCategoryObject cat;
1149                                 suboperation.getCategory(cat);
1150                                 category = cat.getFullName().replace(OBJECTSEPARATOR, QStringLiteral(":"));
1151                             }
1152 
1153                             // Is it a transfer
1154                             SKGOperationObject transfer;
1155                             if (operation.isTransfer(transfer)) {
1156                                 if (!category.isEmpty()) {
1157                                     category.prepend('/');
1158                                 }
1159 
1160                                 SKGAccountObject transferAccount;
1161                                 err = transfer.getParentAccount(transferAccount);
1162                                 IFOK(err) category.prepend('[' % transferAccount.getName() % ']');
1163                             }
1164                             if (!category.isEmpty()) {
1165                                 stream << 'L' << category << SKGENDL;
1166                             }
1167 
1168                             if (nbSubOps > 1) {
1169                                 // Split operation
1170                                 for (int k = 0; k < nbSubOps; ++k) {
1171                                     SKGSubOperationObject suboperation(subtransactions.at(k));
1172                                     SKGCategoryObject cat;
1173                                     suboperation.getCategory(cat);
1174 
1175                                     QString category2 = cat.getFullName().replace(OBJECTSEPARATOR, QStringLiteral(":"));
1176                                     if (!category2.isEmpty()) {
1177                                         stream << 'S' << category2 << SKGENDL;
1178                                     }
1179                                     QString memo2 = suboperation.getComment();
1180                                     memo2 = memo2.trimmed();
1181                                     if (!memo2.isEmpty()) {
1182                                         stream << 'E' << memo2 << SKGENDL;
1183                                     }
1184                                     stream << '$' << SKGServices::doubleToString(suboperation.getQuantity()) << SKGENDL;
1185                                 }
1186                             }
1187                         }
1188 
1189                         stream << '^' << SKGENDL;
1190                     }
1191                     IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
1192                 }
1193 
1194                 SKGENDTRANSACTION(m_importer->getDocument(),  err)
1195             }
1196             IFOKDO(err, m_importer->getDocument()->stepForward(2))
1197 
1198             // Export prices
1199             SKGObjectBase::SKGListSKGObjectBase unitvalues;
1200             IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_unitvalue"), QStringLiteral("1=1 ORDER BY (select t_name from unit where v_unitvalue.rd_unit_id=unit.id), d_date"), unitvalues))
1201             nb = unitvalues.count();
1202             if (!err && (nb != 0)) {
1203                 stream << "!Type:Prices\n";
1204                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export units"), nb);
1205                 for (int i = 0; !err && i < nb; ++i) {
1206                     SKGUnitValueObject unitVal(unitvalues.at(i));
1207                     SKGUnitObject unit;
1208                     err = unitVal.getUnit(unit);
1209                     IFOK(err) {
1210                         QStringList vals;
1211                         QString v = unit.getSymbol();
1212                         if (v.isEmpty()) {
1213                             v = unit.getName();
1214                         }
1215                         vals.push_back(v);
1216                         vals.push_back(SKGServices::doubleToString(unitVal.getQuantity()));
1217                         vals.push_back(SKGServices::dateToSqlString(unitVal.getDate()));
1218 
1219                         stream << SKGServices::stringsToCsv(vals) << SKGENDL;
1220                     }
1221                     IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
1222                 }
1223                 stream << "^" << SKGENDL;
1224 
1225                 SKGENDTRANSACTION(m_importer->getDocument(),  err)
1226             }
1227             IFOKDO(err, m_importer->getDocument()->stepForward(3))
1228             SKGENDTRANSACTION(m_importer->getDocument(),  err)
1229         }
1230 
1231         // Close file
1232         file.commit();
1233     }
1234 
1235     return err;
1236 }
1237 
1238 QString SKGImportPluginQif::getMimeTypeFilter() const
1239 {
1240     return "*.qif|" % i18nc("A file format", "QIF file");
1241 }
1242 
1243 #include <skgimportpluginqif.moc>