1 /***************************************************************************
2  * SPDX-FileCopyrightText: 2021 S. MANKOWSKI stephane@mankowski.fr
3  * SPDX-FileCopyrightText: 2021 G. DE BURE support@mankowski.fr
4  * SPDX-License-Identifier: GPL-3.0-or-later
5  ***************************************************************************/
6 /** @file
7  * This file is Skrooge plugin for QIF import / export.
8  *
9  * @author Stephane MANKOWSKI / Guillaume DE BURE
10  */
11 #include "skgimportpluginqif.h"
12 
13 #include <klocalizedstring.h>
14 
15 #include <kpluginfactory.h>
16 
17 #include <qcryptographichash.h>
18 #include <qfile.h>
19 #include <qsavefile.h>
20 
21 #include "skgbankincludes.h"
22 #include "skgimportexportmanager.h"
23 #include "skgservices.h"
24 #include "skgtraces.h"
25 
26 /**
27 * Opening balance string
28  */
29 #define OPENINGBALANCE QStringLiteral("Opening Balance")
30 
31 /**
32  * This plugin factory.
33  */
K_PLUGIN_FACTORY(SKGImportPluginQifFactory,registerPlugin<SKGImportPluginQif> ();)34 K_PLUGIN_FACTORY(SKGImportPluginQifFactory, registerPlugin<SKGImportPluginQif>();)
35 
36 SKGImportPluginQif::SKGImportPluginQif(QObject* iImporter, const QVariantList& iArg)
37     : SKGImportPlugin(iImporter)
38 {
39     SKGTRACEINFUNC(10)
40     Q_UNUSED(iArg)
41 
42     m_importParameters[QStringLiteral("date_format")] = QString();
43     m_exportParameters[QStringLiteral("uuid_of_selected_accounts_or_operations")] = QString();
44 }
45 
46 SKGImportPluginQif::~SKGImportPluginQif()
47     = default;
48 
isImportPossible()49 bool SKGImportPluginQif::isImportPossible()
50 {
51     SKGTRACEINFUNC(10)
52     return isExportPossible();
53 }
54 
importFile()55 SKGError SKGImportPluginQif::importFile()
56 {
57     if (m_importer == nullptr) {
58         return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
59     }
60 
61     SKGError err;
62     SKGTRACEINFUNCRC(2, err)
63 
64     // Begin transaction
65     err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import %1 file", "QIF"), 3);
66     IFOK(err) {
67         // Create account if needed
68         QDateTime now = QDateTime::currentDateTime();
69         QString postFix = SKGServices::dateToSqlString(now);
70 
71         // Step 1 done
72         IFOKDO(err, m_importer->getDocument()->stepForward(1))
73 
74         // Open file
75         QFile file(m_importer->getLocalFileName());
76         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
77             err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message",  "Open file '%1' failed", m_importer->getFileName().toDisplayString()));
78         } else {
79             QTextStream stream(&file);
80             if (!m_importer->getCodec().isEmpty()) {
81                 stream.setCodec(m_importer->getCodec().toLatin1().constData());
82             }
83 
84             // load file in memory
85             QStringList lines;
86             QStringList dates;
87             bool inWrongSection = false;
88             bool inPriceSection = false;
89             while (!stream.atEnd()) {
90                 // Read line
91                 // Check line if line is empty or is a commented
92                 QString line = stream.readLine().trimmed().toUtf8();
93                 if (!line.isEmpty() && line[0] != '#') {
94                     lines.push_back(line);
95                     // Manage !Account section
96                     if (line.startsWith(QLatin1String("!"))) {
97                         inWrongSection = false;
98                         inPriceSection = false;
99                     }
100 
101                     if (QString::compare(line, QStringLiteral("!account"), Qt::CaseInsensitive) == 0 ||
102                         QString::compare(line, QStringLiteral("!type:cat"), Qt::CaseInsensitive) == 0 ||
103                         QString::compare(line, QStringLiteral("!type:tag"), Qt::CaseInsensitive) == 0 ||
104                         QString::compare(line, QStringLiteral("!type:class"), Qt::CaseInsensitive) == 0) {
105                         inWrongSection = true;
106                     } else if (QString::compare(line, QStringLiteral("!type:prices"), Qt::CaseInsensitive) == 0) {
107                         inPriceSection = true;
108                     }
109 
110                     // We try to find automatically the date format
111                     if (!inWrongSection && line[0] == 'D') {
112                         dates.push_back(line.right(line.length() - 1));
113                     } else if (inPriceSection) {
114                         QStringList vals = SKGServices::splitCSVLine(line, ',');
115                         if (vals.count() == 3) {
116                             dates.push_back(vals.at(2));
117                         }
118                     }
119                 }
120             }
121 
122             // close file
123             file.close();
124 
125             // Select dateformat
126             QString dateFormat = m_importParameters.value(QStringLiteral("date_format"));
127             if (dateFormat.isEmpty()) {
128                 dateFormat = SKGServices::getDateFormat(dates);    // Automatic detection
129             }
130             if (dateFormat.isEmpty()) {
131                 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message",  "Date format not supported"));
132             }
133             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)))
134 
135             // Step 2 done
136             IFOKDO(err, m_importer->getDocument()->stepForward(2))
137 
138             // Treat all lines
139             IFOK(err) {
140                 SKGAccountObject* account = nullptr;
141                 SKGOperationObject currentOperation;
142                 SKGOperationObject payement;
143                 SKGPayeeObject currentPayee;
144                 SKGTrackerObject currentTracker;
145                 SKGUnitObject currentUnit;
146                 SKGSubOperationObject currentSubOperation;
147                 QDate currentOperationDate;
148                 QString lastTransferAccount;
149                 QList<QString> transferAccount;
150                 QList<double> transferQuantity;
151                 bool addNextAmountToTransferQuantity = false;
152                 QString stringForHash;
153                 QString currentUnitForInvestment;
154                 QChar inSection = 'B';
155                 bool currentOperationInitialized = false;
156                 bool latestSubCatMustBeRemoved = false;
157                 bool investmentAccount = false;
158                 bool div = false;
159                 bool automaticAccount = true;
160                 int quantityFactor = 1;
161                 double currentUnitPrice = 1;
162                 double checkOperationAmount = 0;
163                 double checkSuboperationsAmount = 0;
164                 bool openingbalancecreated = false;
165 
166                 int nb = lines.size();
167                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import operations"), nb);
168                 for (int i = 0; !err && i < nb; ++i) {
169                     QString line = lines.at(i);
170                     QString val;
171                     QChar op = line[0];
172                     if (line.length() > 1) {
173                         val = line.right(line.length() - 1).trimmed();
174                     }
175 
176                     // Manage !Account section
177                     if (QString::compare(line, QStringLiteral("!type:bank"), Qt::CaseInsensitive) == 0 ||
178                         QString::compare(line, QStringLiteral("!type:cash"), Qt::CaseInsensitive) == 0 ||
179                         QString::compare(line, QStringLiteral("!type:ccard"), Qt::CaseInsensitive) == 0 ||
180                         QString::compare(line, QStringLiteral("!type:oth a"), Qt::CaseInsensitive) == 0 ||
181                         QString::compare(line, QStringLiteral("!type:oth l"), Qt::CaseInsensitive) == 0 ||
182                         QString::compare(line, QStringLiteral("!type:invst"), Qt::CaseInsensitive) == 0) {
183                         inSection = 'B';
184                         openingbalancecreated = false;
185                         investmentAccount = (QString::compare(val, QStringLiteral("type:invst"), Qt::CaseInsensitive) == 0);
186 
187                         // Set type of account
188                         if (account == nullptr) {
189                             SKGAccountObject defAccount;
190                             err = m_importer->getDefaultAccount(defAccount);
191                             IFOKDO(err, defAccount.addOperation(currentOperation, true))
192                             IFOK(err) account = new SKGAccountObject(defAccount);
193                         }
194 
195                         if (!err && (account != nullptr)) {
196                             err = account->setType(QString::compare(line, QStringLiteral("!type:bank"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::CURRENT :
197                                                    (QString::compare(line, QStringLiteral("!type:ccard"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::CREDITCARD :
198                                                     (QString::compare(line, QStringLiteral("!type:invst"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::INVESTMENT :
199                                                      (QString::compare(line, QStringLiteral("!type:oth a"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::ASSETS : SKGAccountObject::OTHER))));
200                             IFOKDO(err, account->save())
201                         }
202                     } else if (QString::compare(line, QStringLiteral("!account"), Qt::CaseInsensitive) == 0) {
203                         inSection = 'A';
204                         openingbalancecreated = false;
205                         automaticAccount = false;
206                     } else if (QString::compare(line, QStringLiteral("!type:cat"), Qt::CaseInsensitive) == 0) {
207                         inSection = 'C';
208                         openingbalancecreated = false;
209                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Categories found and imported")))
210                     } else if (QString::compare(line, QStringLiteral("!type:prices"), Qt::CaseInsensitive) == 0) {
211                         inSection = 'U';
212                         openingbalancecreated = false;
213                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Units prices found and imported")))
214                     } else if (QString::compare(line, QStringLiteral("!type:security"), Qt::CaseInsensitive) == 0) {
215                         inSection = 'S';
216                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Units found and imported")))
217                     } else if (QString::compare(line, QStringLiteral("!type:tag"), Qt::CaseInsensitive) == 0) {
218                         inSection = 'T';
219                         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Trackers found and imported")))
220                     } else if (line.at(0) == '!') {
221                         inSection = '?';
222                         openingbalancecreated = false;
223                     } else if (inSection == 'U') {
224                         // Unit value creation
225                         openingbalancecreated = false;
226                         QStringList vals = SKGServices::splitCSVLine(line, ',');
227                         if (vals.count() == 3 && !vals.at(0).isEmpty()) {
228                             err = m_importer->getDocument()->addOrModifyUnitValue(vals.at(0), SKGServices::stringToTime(SKGServices::dateToSqlString(vals.at(2), dateFormat)).date(), SKGServices::stringToDouble(vals.at(1)));
229                         }
230 
231                     } else if (inSection == 'T') {
232                         // Tracker creation
233                         if (op == 'N') {
234                             IFOKDO(err, SKGTrackerObject::createTracker(m_importer->getDocument(), val, currentTracker))
235                         } else if (op == 'D') {
236                             IFOKDO(err, currentTracker.setComment(val))
237                             IFOKDO(err, currentTracker.save())
238                         }
239                     } else if (inSection == 'S') {
240                         // Unit creation
241                         if (op == 'N') {
242                             currentUnit = SKGUnitObject(m_importer->getDocument());
243                             IFOKDO(err, currentUnit.setName(val))
244                             IFOKDO(err, currentUnit.setSymbol(val))
245                             IFOKDO(err, currentUnit.setType(SKGUnitObject::CURRENCY))
246                             IFOKDO(err, currentUnit.setNumberDecimal(2))
247                             IFOKDO(err, currentUnit.save())
248                         } else if (op == 'S') {
249                             IFOKDO(err, currentUnit.setSymbol(val))
250                             IFOKDO(err, currentUnit.save())
251                         } else if (op == 'T') {
252                             if (QString::compare(val, QStringLiteral("stock"), Qt::CaseInsensitive) == 0) {
253                                 IFOKDO(err, currentUnit.setType(SKGUnitObject::SHARE))
254                                 IFOKDO(err, currentUnit.setNumberDecimal(4))
255                                 IFOKDO(err, currentUnit.save())
256                             }
257                         }
258                     } else if (inSection == 'C') {
259                         // Category creation
260                         openingbalancecreated = false;
261                         if (op == 'N') {
262                             SKGCategoryObject Category;
263                             val.replace('/', OBJECTSEPARATOR);
264                             val.replace(':', OBJECTSEPARATOR);
265                             err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category);
266                         }
267                     } else if (inSection == 'A') {
268                         // Account creation
269                         openingbalancecreated = false;
270                         if (op == 'N') {
271                             // Check if the account already exist
272                             SKGAccountObject account2;
273                             err = SKGNamedObject::getObjectByName(m_importer->getDocument(), QStringLiteral("account"), val, account2);
274                             IFKO(err) {
275                                 // Create account
276                                 SKGBankObject bank(m_importer->getDocument());
277                                 err = bank.setName(i18nc("Noun",  "Bank for import %1", postFix));
278                                 if (!err && bank.load().isFailed()) {
279                                     err = bank.save(false);
280                                 }
281                                 IFOKDO(err, bank.addAccount(account2))
282                                 IFOKDO(err, account2.setName(val))
283                                 if (!err && account2.load().isFailed()) {
284                                     err = account2.save(false);    // Save only
285                                 }
286                             }
287 
288                             IFOK(err) {
289                                 delete account;
290                                 account = new SKGAccountObject(account2);
291                             }
292                         } else if (op == 'D') {
293                             if (account != nullptr) {
294                                 err = account->setNumber(val);
295                             }
296                         } else if (op == 'T') {
297                             if (account != nullptr) {
298                                 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))));
299                             }
300                         } else if (op == '^') {
301                             // ^     End of entry
302                             // save
303                             if (account != nullptr) {
304                                 err = account->save();
305                             }
306                         }
307                     } else if (inSection == 'B') {
308                         // Operation creation
309                         /*
310                         >>>> Items for Non-Investment Accounts <<<<
311                         DONE    D      Date
312                         DONE    T      Amount
313                             U      Transaction amount (higher possible value than T)
314                         DONE    C      Cleared status
315                         DONE    N      Number (check or reference number)
316                         DONE    P      Payee/description
317                         DONE    M      Memo
318                         DONE    A      Address (up to 5 lines; 6th line is an optional message)
319                         DONE    L      Category (category/class or transfer/class)
320                         DONE    S      Category in split (category/class or transfer/class)
321                         DONE    E      Memo in split
322                         DONE    $      Dollar amount of split
323                             %      Percentage of split if percentages are used
324                             F      Reimbursable business expense flag
325                             X      Small Business extensions
326                         DONE    ^      End of entry
327 
328                         >>>> Items for Investment Accounts <<<<
329                         DONE    D   Date
330                             N   Action
331                         DONE    Y   Security
332                         DONE    I   Price
333                         DONE    Q   Quantity (number of shares or split ratio)
334                         DONE    T   Transaction amount
335                         DONE    C   Cleared status
336                             P   Text in the first line for transfers and reminders
337                         DONE    M   Memo
338                             O   Commission
339                             L   Account for the transfer
340                             $   Amount transferred
341                             ^   End of entry
342                         */
343                         stringForHash += line;
344                         if (op == 'D') {
345                             // D     Date
346                             /*
347                             Dates in US QIF files are usually in the format MM/DD/YY, although
348                             four-digit years are not uncommon.  Dates sometimes occur without the
349                             slash separator, or using other separators in place of the slash,
350                             commonly '-' and '.'.  US Quicken seems to be using the ' to indicate
351                             post-2000 two-digit years (such as 01/01'00 for Jan 1 2000).  Some
352                             banks appear to be using a completely undifferentiated numeric QString
353                             formateed YYYYMMDD in downloaded QIF files.
354                             */
355                             // Operation creation
356                             SKGUnitObject unit;
357                             IFOK(err) {
358                                 if (account != nullptr) {
359                                     err = account->addOperation(currentOperation, true);
360                                     if (!openingbalancecreated) {
361                                         double initBalance;
362                                         account->getInitialBalance(initBalance, unit);
363                                     }
364                                 } else {
365                                     SKGAccountObject defAccount;
366                                     err = m_importer->getDefaultAccount(defAccount);
367                                     IFOKDO(err, defAccount.addOperation(currentOperation, true))
368                                     if (!openingbalancecreated) {
369                                         double initBalance;
370                                         defAccount.getInitialBalance(initBalance, unit);
371                                     }
372                                 }
373                                 currentOperationInitialized = true;
374                             }
375 
376                             // Set date
377                             currentOperationDate = SKGServices::stringToTime(SKGServices::dateToSqlString(val, dateFormat)).date();
378                             IFOKDO(err, currentOperation.setDate(currentOperationDate))
379 
380                             // Set unit
381                             IFOK(err) {
382                                 // Create unit if needed
383                                 // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
384                                 if (!unit.exist()) {
385                                     err = m_importer->getDefaultUnit(unit, &currentOperationDate);
386                                 }
387                                 IFOKDO(err, currentOperation.setUnit(unit))
388                             }
389 
390                             IFOK(err) currentOperation.save();
391 
392                             // Create suboperation
393                             IFOKDO(err, currentOperation.addSubOperation(currentSubOperation))
394                         } else if (op == 'Y') {
395                             // Y     Security
396                             if (!div) {
397                                 currentUnitForInvestment = val;
398 
399                                 SKGUnitObject unit(m_importer->getDocument());
400                                 if (currentUnitForInvestment.isEmpty()) {
401                                     IFOKDO(err, err = m_importer->getDefaultUnit(unit))
402                                 } else {
403                                     IFOKDO(err, unit.setName(currentUnitForInvestment))
404                                     IFOKDO(err, unit.setSymbol(currentUnitForInvestment))
405                                     if (unit.load().isFailed()) {
406                                         IFOKDO(err, unit.setType(investmentAccount ? SKGUnitObject::SHARE : SKGUnitObject::CURRENCY))
407                                         IFOKDO(err, unit.save(false))
408                                     }
409                                 }
410                                 IFOKDO(err, currentOperation.setUnit(unit))
411                             } else {
412                                 // For dividend, if comment is empty, we set the security in comment
413                                 if (currentOperation.getComment().isEmpty()) {
414                                     err = currentOperation.setComment(val);
415                                 }
416                             }
417                         } else if (op == 'O') {
418                             // O     Commission
419                             // Get previous quantity
420                             double quantity = SKGServices::stringToDouble(val);
421                             SKGObjectBase::SKGListSKGObjectBase subops;
422                             payement.getSubOperations(subops);
423                             if (!subops.isEmpty()) {
424                                 SKGSubOperationObject subpayement(subops.at(0));
425                                 err = subpayement.setQuantity(subpayement.getQuantity() + quantity);
426                                 IFOKDO(err, subpayement.save())
427                             }
428 
429                             SKGSubOperationObject subcommission;
430                             if (!payement.exist()) {
431                                 // We have to create a new operation
432                                 if (account != nullptr) {
433                                     err = account->addOperation(payement, true);
434                                 } else {
435                                     SKGAccountObject defAccount;
436                                     err = m_importer->getDefaultAccount(defAccount);
437                                     IFOKDO(err, defAccount.addOperation(payement, true))
438                                 }
439                                 IFOKDO(err, payement.setDate(currentOperationDate))
440                                 IFOK(err) {
441                                     // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
442                                     SKGUnitObject unit;
443                                     if ((account != nullptr) && !openingbalancecreated) {
444                                         double initBalance;
445                                         account->getInitialBalance(initBalance, unit);
446                                     }
447                                     if (!unit.exist()) {
448                                         err = m_importer->getDefaultUnit(unit, &currentOperationDate);
449                                     }
450                                     IFOKDO(err, payement.setUnit(unit))
451                                 }
452                                 IFOKDO(err, payement.save())
453                             }
454                             IFOKDO(err, payement.addSubOperation(subcommission))
455                             IFOKDO(err, subcommission.setQuantity(-quantity))
456                             IFOKDO(err, subcommission.save(false, false))
457                         } else if (op == 'I') {
458                             // I     Price
459                             currentUnitPrice = SKGServices::stringToDouble(val);
460                             if ((currentUnitPrice != 0.0) && !currentUnitForInvestment.isEmpty()) {
461                                 err = m_importer->getDocument()->addOrModifyUnitValue(currentUnitForInvestment, currentOperationDate, currentUnitPrice);
462                             }
463                         } else if (op == 'N') {
464                             if (investmentAccount) {
465                                 // N     Action
466                                 /*
467                                 QIF N Line    Notes
468                                 ============  =====
469                                 Aktab         Same as ShrsOut.
470                                 AktSplit      Same as StkSplit.
471                                 Aktzu         Same as ShrsIn.
472                                 Buy           Buy shares.
473                                 BuyX          Buy shares. Used with an L line.
474                                 Cash          Miscellaneous cash transaction. Used with an L line.
475                                 CGMid         Mid-term capital gains.
476                                 CGMidX        Mid-term capital gains. For use with an L line.
477                                 CGLong        Long-term capital gains.
478                                 CGLongX       Long-term capital gains. For use with an L line.
479                                 CGShort       Short-term capital gains.
480                                 CGShortX      Short-term capital gains. For use with an L line.
481                                 ContribX      Same as XIn. Used for tax-advantaged accounts.
482                                 CvrShrt       Buy shares to cover a short sale.
483                                 CvrShrtX      Buy shares to cover a short sale. Used with an L line.
484                                 Div           Dividend received.
485                                 DivX          Dividend received. For use with an L line.
486                                 Errinerg      Same as Reminder.
487                                 Exercise      Exercise an option.
488                                 ExercisX      Exercise an option. For use with an L line.
489                                 Expire        Mark an option as expired. (Uses D, N, Y & M lines)
490                                 Grant         Receive a grant of stock options.
491                                 Int           Same as IntInc.
492                                 IntX          Same as IntIncX.
493                                 IntInc        Interest received.
494                                 IntIncX       Interest received. For use with an L line.
495                                 K.gewsp       Same as CGShort. (German)
496                                 K.gewspX      Same as CGShortX. (German)2307068
497                                 Kapgew        Same as CGLong. Kapitalgewinnsteuer.(German)
498                                 KapgewX       Same as CGLongX. Kapitalgewinnsteuer. (German)
499                                 Kauf          Same as Buy. (German)
500                                 KaufX         Same as BuyX. (German)
501                                 MargInt       Margin interest paid.
502                                 MargIntX      Margin interest paid. For use with an L line.
503                                 MiscExp       Miscellaneous expense.
504                                 MiscExpX      Miscellaneous expense. For use with an L line.
505                                 MiscInc       Miscellaneous income.
506                                 MiscIncX      Miscellaneous income. For use with an L line.
507                                 ReinvDiv      Reinvested dividend.
508                                 ReinvInt      Reinvested interest.
509                                 ReinvLG       Reinvested long-term capital gains.
510                                 Reinvkur      Same as ReinvLG.
511                                 Reinvksp      Same as ReinvSh.
512                                 ReinvMd       Reinvested mid-term capital gains.
513                                 ReinvSG       Same as ReinvSh.
514                                 ReinvSh       Reinvested short-term capital gains.
515                                 Reinvzin      Same as ReinvDiv.
516                                 Reminder      Reminder. (Uses D, N, C & M lines)
517                                 RtrnCap       Return of capital.
518                                 RtrnCapX      Return of capital. For use with an L line.
519                                 Sell          Sell shares.
520                                 SellX         Sell shares. For use with an L line.
521                                 ShtSell       Short sale.
522                                 ShrsIn        Deposit shares.
523                                 ShrsOut       Withdraw shares.
524                                 StkSplit      Share split.
525                                 Verkauf       Same as Sell. (German)
526                                 VerkaufX      Same as SellX. (German)
527                                 Vest          Mark options as vested. (Uses N, Y, Q, C & M lines)
528                                 WithDrwX      Same as XOut. Used for tax-advantaged accounts.
529                                 XIn           Transfer cash from another account.
530                                 XOut          Transfer cash to another account.
531                                 */
532                                 val = val.toLower();
533                                 if (val.contains(QStringLiteral("div")) && val != QStringLiteral("reinvdiv")) {
534                                     // TODO(Stephane MANKOWSKI) err=currentOperation.setProperty ( "SKG_OP_ORIGINAL_AMOUNT", "" );
535                                     div = true;
536                                 } else if (val.contains(QStringLiteral("sell")) ||
537                                            val.contains(QStringLiteral("verkauf")) ||
538                                            val.contains(QStringLiteral("miscexp")) ||
539                                            val.contains(QStringLiteral("shrsout"))
540                                           ) {
541                                     quantityFactor = -1;
542                                 }
543                                 // Correction 214851 vvvv
544                                 // err=currentOperation.setComment ( val );
545                                 // if ( !err ) err=currentOperation.setMode ( i18nc ( "Noun, the title of an item","Title" ) );
546                                 // Correction 214851 ^^^^
547                             } else {
548                                 // N     Num (check or reference number)
549                                 // Set number
550                                 bool ok;
551                                 int number = val.toInt(&ok);
552                                 if (ok && number != 0) {
553                                     err = currentOperation.setNumber(val);
554                                 } else {
555                                     err = currentOperation.setMode(val);
556                                 }
557                             }
558                         } else if (op == 'Q') {
559                             // Q     Quantity (number of shares or split ratio)
560                             // Set value
561                             if (!val.isEmpty()) {
562                                 double previousQuantity = currentSubOperation.getQuantity();
563                                 if (previousQuantity != 0.0) {
564                                     // We have to create a new operation
565                                     if (account != nullptr) {
566                                         err = account->addOperation(payement, true);
567                                     } else {
568                                         SKGAccountObject defAccount;
569                                         err = m_importer->getDefaultAccount(defAccount);
570                                         IFOKDO(err, defAccount.addOperation(payement, true))
571                                     }
572                                     IFOKDO(err, payement.setDate(currentOperationDate))
573                                     IFOK(err) {
574                                         // Create unit if needed
575                                         // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
576                                         SKGUnitObject unit;
577                                         if ((account != nullptr) && !openingbalancecreated) {
578                                             double initBalance;
579                                             account->getInitialBalance(initBalance, unit);
580                                         }
581                                         if (!unit.exist()) {
582                                             err = m_importer->getDefaultUnit(unit, &currentOperationDate);
583                                         }
584                                         IFOKDO(err, payement.setUnit(unit))
585                                     }
586                                     IFOKDO(err, payement.save())
587                                     IFOKDO(err, currentOperation.setGroupOperation(payement))
588 
589                                     SKGSubOperationObject subpayement;
590                                     IFOKDO(err, payement.addSubOperation(subpayement))
591                                     IFOKDO(err, subpayement.setQuantity(-previousQuantity))
592                                     IFOKDO(err, subpayement.save())
593                                 }
594 
595                                 IFOKDO(err, currentSubOperation.setQuantity(quantityFactor * SKGServices::stringToDouble(val)))
596                             }
597                         } else if (op == 'T') {
598                             // T     Amount
599                             // Set value
600                             checkOperationAmount = SKGServices::stringToDouble(val);
601                             err = currentSubOperation.setQuantity(checkOperationAmount / currentUnitPrice);
602                             if (!err && investmentAccount) {
603                                 err = currentOperation.setProperty(QStringLiteral("SKG_OP_ORIGINAL_AMOUNT"), val);
604                             }
605                         } else if (op == '$') {
606                             // Dollar amount of split
607                             // Set value
608                             if (!investmentAccount) {
609                                 double vald = SKGServices::stringToDouble(val);
610                                 checkSuboperationsAmount += vald;
611                                 if (addNextAmountToTransferQuantity && !lastTransferAccount.isEmpty()) {
612                                     transferQuantity[transferAccount.count() - 1] += vald;
613                                 }
614                                 addNextAmountToTransferQuantity = false;
615                                 lastTransferAccount = QString();
616                                 err = currentSubOperation.setQuantity(vald);
617 
618                                 // save
619                                 IFOKDO(err, currentSubOperation.save())
620 
621                                 // Create suboperation
622                                 IFOKDO(err, currentOperation.addSubOperation(currentSubOperation))
623 
624                                 latestSubCatMustBeRemoved = true;
625                             }
626                         } else if (op == 'P') {
627                             // P Payee
628                             // Set Payee
629                             // Clean QIF coming from bankperfect
630                             val.remove(QStringLiteral("[auto]"));
631 
632                             err = SKGPayeeObject::createPayee(m_importer->getDocument(), val, currentPayee);
633                             IFOKDO(err, currentOperation.setPayee(currentPayee))
634                         } else if (op == 'A') {
635                             // A      Address (up to 5 lines; 6th line is an optional message)
636                             QString add = currentPayee.getAddress();
637                             if (!add.isEmpty()) {
638                                 add += ' ';
639                             }
640                             add += val;
641                             err = currentPayee.setAddress(add);
642                             IFOKDO(err, currentPayee.save())
643                         } else if (op == 'M') {
644                             // M     Memo
645                             // Set Memo
646                             err = currentOperation.setComment(val);
647                         } else if (op == 'E') {
648                             // E     Memo in split
649                             // Set Memo
650                             err = currentSubOperation.setComment(val);
651                         } else if (op == 'S' || op == 'L') {
652                             // S     Category in split (Category/Transfer/Class)
653                             // L     Category (Category/Subcategory/Transfer/Class)
654                             // LCategory of transaction
655                             // L[Transfer account]
656                             // LCategory of transaction/Class of transaction
657                             // L[Transfer account]/Class of transaction// Set Category
658                             if (!val.isEmpty()) {
659                                 if (val[0] == '[') {
660                                     addNextAmountToTransferQuantity = true;
661 
662                                     int pos = val.indexOf(']');
663                                     if (pos != -1) {
664                                         SKGPayeeObject payeeObj;
665                                         currentOperation.getPayee(payeeObj);
666                                         bool opening = (payeeObj.getName().compare(OPENINGBALANCE, Qt::CaseInsensitive) == 0);
667 
668                                         // 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
669                                         if (op == 'L' && automaticAccount && (account != nullptr) && opening) {
670                                             QString accountName = val.mid(1, pos - 1);
671 
672                                             SKGAccountObject newAccount(m_importer->getDocument());
673                                             err = newAccount.setName(accountName);
674                                             IFOK(err) {
675                                                 if (newAccount.exist()) {
676                                                     // Oups, the real account is existing and it is another one
677                                                     err = newAccount.load();
678 
679                                                     // We move the operation in the right account
680                                                     IFOKDO(err, currentOperation.setParentAccount(newAccount))
681                                                     IFOKDO(err, currentOperation.save())
682 
683                                                     // We delete the previous account if empty
684                                                     IFOK(err) {
685                                                         if (account->getNbOperation() == 0) {
686                                                             err = account->remove();
687                                                         }
688                                                         delete account;
689                                                         account = new SKGAccountObject(newAccount);
690                                                     }
691                                                 } else {
692                                                     err = account->setName(accountName);
693                                                     IFOKDO(err, account->save())
694                                                 }
695                                             }
696                                         }
697 //                                            if ( op=='L' && currentOperation.getPayee().compare ( "Opening Balance", Qt::CaseInsensitive ) !=0 && !investmentAccount)
698                                         if (!opening) {
699                                             lastTransferAccount = val.mid(1, pos - 1);
700                                             if ((account != nullptr) && lastTransferAccount == account->getName()) {
701                                                 lastTransferAccount = QString();
702                                             }
703 
704                                             if (!lastTransferAccount.isEmpty() &&
705                                                 (transferAccount.count() == 0 ||
706                                                  transferAccount.at(transferAccount.count() - 1) != lastTransferAccount ||
707                                                  transferQuantity.at(transferQuantity.count() - 1) != 0.0
708                                                 )
709                                                ) {
710                                                 transferAccount.append(lastTransferAccount);
711                                                 transferQuantity.append(0.0);
712                                             }
713                                         }
714                                         val = val.mid(pos + 2);
715                                     }
716                                 }
717                                 if (!err && !val.isEmpty()) {
718                                     auto cat_tag = SKGServices::splitCSVLine(val, '/', false);
719                                     val = cat_tag.at(0);
720                                     SKGCategoryObject Category;
721                                     val.replace('/', OBJECTSEPARATOR);
722                                     val.replace(':', OBJECTSEPARATOR);
723                                     val.replace(',', OBJECTSEPARATOR);
724                                     val.replace(';', OBJECTSEPARATOR);
725                                     err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category);
726                                     IFOKDO(err, currentSubOperation.setCategory(Category))
727 
728                                     if (!err && cat_tag.count() > 1) {
729                                         SKGTrackerObject tracker;
730                                         err = SKGTrackerObject::createTracker(m_importer->getDocument(), cat_tag.at(1), tracker);
731                                         IFOKDO(err, currentSubOperation.setTracker(tracker))
732                                     }
733                                 }
734                             }
735                         } else if (op == 'C') {
736                             // C     Cleared status
737                             // Set status
738                             err = currentOperation.setStatus((val == QStringLiteral("C") || val == QStringLiteral("*") ? SKGOperationObject::POINTED : (val == QStringLiteral("R") || val == QStringLiteral("X") ? SKGOperationObject::CHECKED : SKGOperationObject::NONE)));
739                         } else if (op == '^') {
740                             // ^     End of entry
741                             // save
742 
743                             if (currentOperationInitialized) {
744                                 QByteArray hash = QCryptographicHash::hash(stringForHash.toUtf8(), QCryptographicHash::Md5);
745                                 SKGPayeeObject payeeObj;
746                                 currentOperation.getPayee(payeeObj);
747                                 bool opening = (payeeObj.getName().compare(OPENINGBALANCE, Qt::CaseInsensitive) == 0);
748                                 if (!err && opening) {
749                                     // Specific values for initial balance
750                                     err = currentOperation.setStatus(SKGOperationObject::CHECKED);
751                                     IFOKDO(err, currentOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
752                                     IFOKDO(err, currentSubOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
753                                     openingbalancecreated = true;
754                                 }
755 
756                                 IFOKDO(err, currentOperation.setImportID(hash.toHex()))
757                                 IFOKDO(err, currentOperation.save())
758                                 if (!latestSubCatMustBeRemoved && !err) {
759                                     err = currentSubOperation.save();
760                                 }
761 
762                                 // Create transfers if needed
763                                 // Get origin op
764                                 SKGOperationObject opOrigin(m_importer->getDocument(), currentOperation.getID());
765                                 SKGAccountObject accountOrigin;
766                                 IFOKDO(err, opOrigin.getParentAccount(accountOrigin))
767                                 int nbTransfers = transferAccount.count();
768                                 for (int j = 0; !err && j < nbTransfers; ++j) {
769                                     bool merged = false;
770                                     double tq = transferQuantity.at(j);
771                                     const QString& ta = transferAccount.at(j);
772                                     // Is the transfert operation already existing?
773                                     double qua = tq == 0.0 && addNextAmountToTransferQuantity ? SKGServices::stringToDouble(opOrigin.getAttribute(QStringLiteral("f_QUANTITY"))) : tq;
774                                     QString wc = "t_ACCOUNT='" % SKGServices::stringToSqlString(ta) %
775                                                  "' AND t_TOACCOUNT='" % SKGServices::stringToSqlString(accountOrigin.getName()) %
776                                                  "' AND ABS(f_QUANTITY-(" % SKGServices::doubleToString(-qua) % "))<0.0001"
777                                                  " AND ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "'))<1"
778                                                  " ORDER BY ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "')) ASC";
779                                     SKGObjectBase::SKGListSKGObjectBase obs;
780                                     m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display"), wc, obs);
781                                     if (!obs.isEmpty()) {
782                                         // We have to merge them and we do not need to create the transfer
783                                         SKGOperationObject firstOne(obs.at(0));
784 
785                                         // Remove all operation attached to this transfer
786                                         SKGObjectBase::SKGListSKGObjectBase list;
787                                         IFOKDO(err, firstOne.getGroupedOperations(list))
788                                         for (const auto& o : qAsConst(list)) {
789                                             SKGOperationObject op2(o);
790                                             if (op2 != firstOne) {
791                                                 IFOKDO(err, op2.setStatus(SKGOperationObject::NONE))
792                                                 IFOKDO(err, op2.remove(false, true))
793                                             }
794                                         }
795 
796                                         // Attach myself
797                                         IFOKDO(err, currentOperation.setGroupOperation(firstOne))
798                                         IFOKDO(err, currentOperation.save())
799 
800                                         merged = true;
801                                     } else {
802                                         // Is the operation already created as a transfer of an other one?
803                                         QString wc = "t_import_id='QIF TRANSFER-" % SKGServices::stringToSqlString(ta) % "' AND t_ACCOUNT='" % SKGServices::stringToSqlString(accountOrigin.getName()) %
804                                                      "' AND (ABS(f_CURRENTAMOUNT-(" % SKGServices::doubleToString(opOrigin.getCurrentAmount()) % "))<0.0001 OR f_QUANTITY=" % SKGServices::doubleToString(qua) % ")"
805                                                      " AND ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "'))<1"
806                                                      " ORDER BY ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "')) ASC";
807                                         m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display"), wc, obs);
808                                         if (!obs.isEmpty()) {
809                                             // We have to merge them and we do not need to create the transfer
810                                             SKGOperationObject firstOne(obs.at(0));
811                                             err = opOrigin.setStatus(SKGOperationObject::NONE);  // To be sure we can delete it
812                                             IFOKDO(err, opOrigin.save())
813                                             IFOKDO(err, firstOne.mergeAttribute(opOrigin))
814 
815                                             SKGObjectBase::SKGListSKGObjectBase list;
816                                             IFOKDO(err, currentOperation.getGroupedOperations(list))
817                                             for (const auto& o : qAsConst(list)) {
818                                                 SKGOperationObject op2(o);
819                                                 IFOKDO(err, op2.setStatus(SKGOperationObject::NONE))
820                                                 IFOKDO(err, op2.remove(false, true))
821                                             }
822                                             merged = true;
823                                         }
824                                     }
825 
826                                     if (!merged) {
827                                         // Create target account if needed
828                                         SKGAccountObject accountTransfer(m_importer->getDocument());
829                                         if (m_accountCache.contains(ta)) {
830                                             accountTransfer = m_accountCache[ta];
831                                         } else {
832                                             accountTransfer.setName(ta);
833                                             if (!accountTransfer.exist()) {
834                                                 // The account is created in the same bank by default
835                                                 SKGBankObject bankOrigin;
836                                                 IFOKDO(err, accountOrigin.getBank(bankOrigin))
837                                                 IFOKDO(err, accountTransfer.setBank(bankOrigin))
838                                                 IFOKDO(err, accountTransfer.save(false, true))
839                                             } else {
840                                                 err = accountTransfer.load();
841                                             }
842 
843                                             m_accountCache[ta] = accountTransfer;
844                                         }
845 
846                                         // Create operation
847                                         SKGUnitObject unit;
848                                         opOrigin.getUnit(unit);
849 
850                                         SKGOperationObject opTransfer;
851                                         IFOKDO(err, accountTransfer.addOperation(opTransfer, true))
852                                         IFOKDO(err, opTransfer.setDate(opOrigin.getDate()))
853                                         IFOKDO(err, opTransfer.setComment(opOrigin.getComment()))
854                                         SKGPayeeObject payeeObj2;
855                                         opTransfer.getPayee(payeeObj2);
856                                         IFOKDO(err, opTransfer.setPayee(payeeObj2))
857                                         IFOKDO(err, opTransfer.setStatus(opOrigin.getStatus()))
858                                         IFOKDO(err, opTransfer.setUnit(unit))
859                                         IFOKDO(err, opTransfer.setImportID("QIF TRANSFER-" % accountOrigin.getName()))
860                                         IFOKDO(err, opTransfer.save())  // save needed before setGroupOperation
861                                         IFOKDO(err, opTransfer.setGroupOperation(opOrigin))
862                                         IFOKDO(err, opOrigin.load())  // Must be reload because of setGroupOperation modified it
863                                         IFOKDO(err, opTransfer.save())
864 
865                                         SKGSubOperationObject subopTransfer;
866                                         IFOKDO(err, opTransfer.addSubOperation(subopTransfer))
867                                         IFOKDO(err, subopTransfer.setQuantity(-qua))
868                                         IFOKDO(err, subopTransfer.save())
869                                     }
870                                 }
871                             }
872 
873                             // Check Sum($)=T for incident 214462
874                             QString checkOperationAmountString = SKGServices::doubleToString(checkOperationAmount);
875                             QString checkSuboperationsAmountString = SKGServices::doubleToString(checkSuboperationsAmount);
876                             if (!err && checkOperationAmount != 0 && checkSuboperationsAmount != 0 && checkOperationAmountString != checkSuboperationsAmountString) {
877                                 SKGSubOperationObject suboprepair;
878                                 IFOKDO(err, currentOperation.addSubOperation(suboprepair))
879                                 IFOKDO(err, suboprepair.setQuantity(checkOperationAmount - checkSuboperationsAmount))
880                                 IFOKDO(err, suboprepair.setComment(i18nc("An information message",  "Auto repaired operation")))
881                                 IFOKDO(err, suboprepair.save())
882 
883                                 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "The total amount of the operation (%1) was different to the sum of the sub-operations (%2). The operation has been repaired.", checkOperationAmountString, checkSuboperationsAmountString), SKGDocument::Warning))
884                             }
885 
886                             // Initialize variables
887                             currentOperationInitialized = false;
888                             latestSubCatMustBeRemoved = false;
889                             currentUnitForInvestment = QString();
890                             quantityFactor = 1;
891                             currentUnitPrice = 1;
892                             stringForHash = QString();
893                             checkOperationAmount = 0;
894                             checkSuboperationsAmount = 0;
895                             lastTransferAccount = QString();
896                             transferAccount.clear();
897                             transferQuantity.clear();
898                             payement = SKGOperationObject();
899                         } else {
900                             // A    Address (up to five lines; the sixth line is an optional message)
901                         }
902                     }
903 
904                     if (!err && i % 500 == 0) {
905                         err = m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE"));
906                     }
907                     IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
908                 }
909 
910                 delete account;
911                 account = nullptr;
912                 SKGENDTRANSACTION(m_importer->getDocument(),  err)
913 
914                 // Lines treated
915                 IFOKDO(err, m_importer->getDocument()->stepForward(3))
916             }
917         }
918     }
919     SKGENDTRANSACTION(m_importer->getDocument(),  err)
920 
921     return err;
922 }
923 
isExportPossible()924 bool SKGImportPluginQif::isExportPossible()
925 {
926     SKGTRACEINFUNC(10)
927     return (m_importer == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("QIF"));
928 }
929 
exportFile()930 SKGError SKGImportPluginQif::exportFile()
931 {
932     if (m_importer == nullptr) {
933         return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
934     }
935     SKGError err;
936     SKGTRACEINFUNCRC(2, err)
937 
938     // Read parameters
939     auto listUUIDs = SKGServices::splitCSVLine(m_exportParameters.value(QStringLiteral("uuid_of_selected_accounts_or_operations")));
940 
941     QStringList listOperationsToExport;
942     listOperationsToExport.reserve(listUUIDs.count());
943     QStringList listAccountsToExport;
944     listAccountsToExport.reserve(listUUIDs.count());
945     for (const auto& uuid : qAsConst(listUUIDs)) {
946         if (uuid.endsWith(QLatin1String("-operation"))) {
947             listOperationsToExport.push_back(uuid);
948         } else if (uuid.endsWith(QLatin1String("-account"))) {
949             listAccountsToExport.push_back(uuid);
950         }
951     }
952 
953     if ((listAccountsToExport.count() != 0) || (listOperationsToExport.count() != 0)) {
954         IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message",  "Only selected accounts and operations have been exported")))
955     }
956 
957     // Open file
958     QSaveFile file(m_importer->getLocalFileName(false));
959     if (!file.open(QIODevice::WriteOnly)) {
960         err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message",  "Save file '%1' failed", m_importer->getFileName().toDisplayString()));
961     } else {
962         QTextStream stream(&file);
963         if (!m_importer->getCodec().isEmpty()) {
964             stream.setCodec(m_importer->getCodec().toLatin1().constData());
965         }
966 
967         err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export %1 file", "QIF"), 3);
968         IFOK(err) {
969             // Export categories
970             SKGObjectBase::SKGListSKGObjectBase categories;
971             IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_category_display_tmp"), QStringLiteral("1=1 ORDER BY t_fullname, id"), categories))
972             int nbcat = categories.count();
973             if (!err && (nbcat != 0)) {
974                 stream << "!Type:Cat\n";
975                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export categories"), nbcat);
976                 for (int i = 0; !err && i < nbcat; ++i) {
977                     SKGCategoryObject cat(categories.at(i));
978                     QString catName = cat.getFullName();
979                     if (!catName.isEmpty()) {
980                         stream << QStringLiteral("N") << catName.replace(OBJECTSEPARATOR, QStringLiteral(":")) << SKGENDL;
981                         if (SKGServices::stringToDouble(cat.getAttribute(QStringLiteral("f_REALCURRENTAMOUNT"))) < 0) {
982                             stream << "E" << SKGENDL;
983                         } else {
984                             stream << "I" << SKGENDL;
985                         }
986                         stream << "^" << SKGENDL;
987                     }
988                     IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
989                 }
990 
991                 SKGENDTRANSACTION(m_importer->getDocument(),  err)
992             }
993             IFOKDO(err, m_importer->getDocument()->stepForward(1))
994 
995             SKGServices::SKGUnitInfo primaryUnit = m_importer->getDocument()->getPrimaryUnit();
996 
997             // Get operations
998             QString currentAccountName;
999             SKGObjectBase::SKGListSKGObjectBase operations;
1000             IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display_all"), QStringLiteral("t_template='N' ORDER BY t_ACCOUNT, d_date, id"), operations))
1001             int nb = operations.count();
1002             IFOK(err) {
1003                 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export operations"), nb);
1004                 for (int i = 0; !err && i < nb; ++i) {
1005                     SKGOperationObject operation(operations.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::POINTED ? "C" : (status == SKGOperationObject::CHECKED ? "R" : "")) << SKGENDL;
1132 
1133                         // Get sub operations
1134                         SKGObjectBase::SKGListSKGObjectBase suboperations;
1135                         err = operation.getSubOperations(suboperations);
1136                         IFOK(err) {
1137                             int nbSubOps = suboperations.size();
1138                             QString category;
1139                             if (nbSubOps == 1) {
1140                                 SKGSubOperationObject suboperation(suboperations.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(suboperations.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 
getMimeTypeFilter() const1238 QString SKGImportPluginQif::getMimeTypeFilter() const
1239 {
1240     return "*.qif|" % i18nc("A file format", "QIF file");
1241 }
1242 
1243 #include <skgimportpluginqif.moc>
1244