/***************************************************************************
 *   Copyright (C) 2008 by S. MANKOWSKI / G. DE BURE support@mankowski.fr  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>  *
 ***************************************************************************/
/** @file
 * This file implements classes SKGDocument.
 *
 * @author Stephane MANKOWSKI / Guillaume DE BURE
 */
#include "skgdocument.h"
#include "skgtraces.h"
#include "skgerror.h"
#include "skgservices.h"
#include "skgpropertyobject.h"
#include "skgtransactionmng.h"
#include "skgreport.h"

#include <klocale.h>
#include <kicon.h>
#include <kurl.h>
#include <kcolorscheme.h>
#include <kglobal.h>

#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlDriver>
#include <QHash>
#include <QRegExp>
#include <QUuid>
#include <QVariant>
#include <QFile>
#include <QDir>
#include <QDBusConnection>
#include <QDBusMetaType>
#include <QApplication>
#include <qtconcurrentmap.h>

#include <cmath>

#ifdef Q_OS_WIN
#define isnan(a) _isnan(a)
#define isinf(a) !_finite(a)
#else
using std::isnan;
using std::isinf;
#endif

SKGError SKGDocument::m_lastCallbackError;
int SKGDocument::m_databaseUniqueIdentifier = 0;

SKGDocument::SKGDocument()
    : QObject(), m_lastSavedTransaction(0), m_progressFunction(NULL), m_progressData(NULL),
      m_currentFileName(""), m_currentDatabase(NULL),  m_inundoRedoTransaction(0), m_currentTransaction(0),
      m_inProgress(false), m_directAccessDb(false), m_modeReadOnly(false)
{
    SKGTRACEIN(10, "SKGDocument::SKGDocument");

    //DBUS registration
    QDBusConnection dbus = QDBusConnection::sessionBus();
    dbus.registerObject("/skg/skgdocument", this, QDBusConnection::ExportAllContents);
    dbus.registerService("org.skg");

    qDBusRegisterMetaType<SKGError>();

    //Initialisation of undoable tables
    SKGListNotUndoable.push_back("T.doctransaction");
    SKGListNotUndoable.push_back("T.doctransactionitem");
    SKGListNotUndoable.push_back("T.doctransactionmsg");

    //Database unique identifier
    ++m_databaseUniqueIdentifier;
    m_databaseIdentifier = "SKGDATABASE_" % SKGServices::intToString(m_databaseUniqueIdentifier);

    //Initialisation of backup file parameters
    setBackupParameters("", ".old");
}

SKGDocument::~SKGDocument()
{
    SKGTRACEIN(10, "SKGDocument::~SKGDocument");
    close();
    m_progressFunction = NULL;
    m_progressData = NULL;
}

QString SKGDocument::getUniqueIdentifier()
{
    return m_uniqueIdentifier;
}

QString SKGDocument::getDatabaseIdentifier()
{
    return m_databaseIdentifier;
}

SKGError SKGDocument::setProgressCallback(int (*iProgressFunction)(int, qint64, const QString&, void*), void* iData)
{
    SKGError err;
    m_progressFunction = (void*) iProgressFunction;
    m_progressData = iData;

    return err;
}

SKGError SKGDocument::stepForward(int iPosition, const QString& iText)
{
    SKGError err;

    //Increase the step for the last transaction
    if (getDepthTransaction()) {
        m_posStepForTransaction.pop_back();
        m_posStepForTransaction.push_back(iPosition);
    }

    //Check if a callback function exists
    if (m_progressFunction) {
        //YES ==> compute
        double min = 0;
        double max = 100;

        bool emitevent = true;
        SKGIntListIterator nbIt = m_nbStepForTransaction.begin();
        SKGIntListIterator posIt = m_posStepForTransaction.begin();
        for (; emitevent && nbIt != m_nbStepForTransaction.end(); ++nbIt) {
            int p = *posIt;
            int n = *nbIt;
            if (p < 0 || p > n) p = n;

            if (n != 0) {
                double pmin = min;
                double pmax = max;
                min = pmin + (pmax - pmin) * (static_cast<double>(p) / static_cast<double>(n));
                max = pmin + (pmax - pmin) * (static_cast<double>(p + 1) / static_cast<double>(n));
                if (max > 100) max = 100;
            } else emitevent = false;

            ++posIt;
        }

        int posPercent = (int) min;

        //Call the call back
        if (emitevent) {
            m_inProgress = true;
            QString text;
            qint64 time = QDateTime::currentMSecsSinceEpoch() - m_timeBeginTransaction;
            if (time >  3000) {
                text = iText;
                if (text.isEmpty()) text = m_nameForTransaction.at(m_nameForTransaction.count() - 1);
            }
            if ((*(int (*)(int, qint64, const QString&, void*)) m_progressFunction)(posPercent, time, text, m_progressData) != 0) {
                err.setReturnCode(ERR_ABORT);
                err.setMessage(i18nc("User interrupted something that Skrooge was performing", "The current operation has been interrupted"));

                //Remove all untransactionnal message
                m_unTransactionnalMessages.clear();
            }
            m_inProgress = false;
        }
    }
    return err;
}

SKGError SKGDocument::beginTransaction(const QString& iName, int iNbStep, const QDateTime& iDate, bool iRefreshViews)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::beginTransaction", err);
    SKGTRACEL(10) << "Input parameter [name]=[" << iName << "]  [nb step]=[" << iNbStep << "]  [refresh]=[" << (iRefreshViews ? "Y" : "N") << ']' << endl;
    if (m_nbStepForTransaction.size() == 0) {
        //Open SQLtransaction
        if (qApp->type() != QApplication::Tty) QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
        err = executeSqliteOrder("BEGIN;");
        if (!err) {
            //Create undo redo transaction
            err = executeSqliteOrder(QString("insert into doctransaction (d_date, t_name, i_parent") %
                                     (!iRefreshViews ? ", t_refreshviews" : "") %
                                     ") values "
                                     "('" % SKGServices::timeToString(iDate) %
                                     "','" % SKGServices::stringToSqlString(iName) %
                                     "', " % SKGServices::intToString(getTransactionToProcess(SKGDocument::UNDO)) %
                                     (!iRefreshViews ? ",'N'" : "") %
                                     ");");
            addValueInCache("SKG_REFRESH_VIEW", (iRefreshViews ? "Y" : "N"));
            m_currentTransaction = getTransactionToProcess(SKGDocument::UNDO);
            m_timeBeginTransaction = QDateTime::currentMSecsSinceEpoch();
        }
    } else {
        //A transaction already exists
        //Check if the child transaction is a opened in the progress callback
        if (m_inProgress) {
            err.setReturnCode(ERR_FAIL);
            err.setMessage(i18nc("Something went wrong with SQL transactions", "A transaction cannot be started during execution of another one"));
        }
    }
    if (!err) {
        m_nbStepForTransaction.push_back(iNbStep);
        m_posStepForTransaction.push_back(iNbStep);
        QString n = iName;
        n = n.remove("#INTERNAL#");
        if (n.isEmpty() && m_nameForTransaction.count()) n = m_nameForTransaction.at(m_nameForTransaction.count() - 1);
        m_nameForTransaction.push_back(n);

        if (iNbStep) err = stepForward(0);
    } else {
        executeSqliteOrder("ROLLBACK;");
    }
    return err;
}

SKGError SKGDocument::checkExistingTransaction() const
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::checkExistingTransaction", err);
    if (getDepthTransaction() <= 0) {
        err.setReturnCode(ERR_ABORT);
        err.setMessage(i18nc("Something went wrong with SQL transactions", "A transaction must be opened to do this action"));
    }
    return err;
}

SKGError SKGDocument::endTransaction(bool succeedded)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::endTransaction", err);
    if (m_nbStepForTransaction.size() == 0) {
        //Set error message
        err.setReturnCode(ERR_ABORT);
        err.setMessage(i18nc("Something went wrong with SQL transactions", "Closing transaction failed because too many transactions ended"));
    } else {
        stepForward(m_nbStepForTransaction.at(m_nbStepForTransaction.count() - 1)); //=100%
        if (m_nbStepForTransaction.size()) { //This test is needed. It's a security in some cases.
            m_nbStepForTransaction.pop_back();
            m_posStepForTransaction.pop_back();
            m_nameForTransaction.pop_back();

        }
        QString currentTransactionString = SKGServices::intToString(getCurrentTransaction());

        if (m_nbStepForTransaction.size() == 0) {
            QStringList listModifiedTables;
            if (succeedded) {
                //Link items on current transaction
                if (!err) {
                    err = executeSqliteOrder("UPDATE doctransactionitem set rd_doctransaction_id=" % currentTransactionString % " WHERE rd_doctransaction_id=0;");
                }

                //Optimization of the current transaction
                if (!err) {
                    SKGStringListList listTmp;
                    err = executeSelectSqliteOrder("SELECT count(1) FROM doctransactionitem where rd_doctransaction_id=" % currentTransactionString, listTmp);
                    if (!err) {
                        int nbItem = SKGServices::stringToInt(listTmp.at(1).at(0));
                        if (nbItem == 0) {
                            //Optimization is needed
                            //Get popup messages
                            QStringList popupMessages;
                            getMessages(getCurrentTransaction(), popupMessages, false);

                            //Delete current transaction
                            err = executeSqliteOrder("DELETE FROM doctransaction WHERE id=" % currentTransactionString);

                            foreach(const QString & msg, popupMessages) {
                                m_unTransactionnalMessages.push_back(msg);
                            }
                        }
                    }

                }

                //Optimization 2: remove duplicate orders
                if (!err) {
                    err = executeSqliteOrder(
                              "DELETE FROM doctransactionitem WHERE id IN "
                              "(SELECT a.id FROM doctransactionitem a, doctransactionitem b "
                              "WHERE a.rd_doctransaction_id=" % currentTransactionString %
                              " AND b.rd_doctransaction_id=a.rd_doctransaction_id AND a.i_object_id=b.i_object_id "
                              "AND a.t_object_table=b.t_object_table AND "
                              "b.t_action=a.t_action AND b.t_sqlorder=a.t_sqlorder AND a.id>b.id );");
                }

                //Get current transaction information to be able to emit envent in case of SKG_UNDO_MAX_DEPTH=0
                if (!err) {
                    err = this->getDistinctValues("doctransactionitem",
                                                  "t_object_table",
                                                  "rd_doctransaction_id=" % currentTransactionString,
                                                  listModifiedTables);
                }

                //Remove oldest transaction
                if (!err) {
                    QString maxdepthstring = getParameter("SKG_UNDO_MAX_DEPTH");
                    if (maxdepthstring.isEmpty()) maxdepthstring = "-1";
                    int maxdepth = SKGServices::stringToInt(maxdepthstring);
                    if (maxdepth >= 0) {
                        err = executeSqliteOrder("delete from doctransaction where id in (select id from doctransaction limit max(0,((select count(1) from doctransaction)-(" % maxdepthstring % "))))");
                    }
                }

                //Remove SKGDocument::REDO transactions if we are not in a undo / redo transaction
                if (!m_inundoRedoTransaction) {
                    int i = 0;
                    while ((i = getTransactionToProcess(SKGDocument::REDO)) && !err) {
                        err = executeSqliteOrder("delete from doctransaction where id=" % SKGServices::intToString(i));
                    }
                }

                //Commit the transaction
                if (!err) {
                    err = executeSqliteOrder("COMMIT;");
                }

            }

            if (!succeedded || err) {
                //Rollback the transaction
                SKGError err2 = executeSqliteOrder("ROLLBACK;");
                if (!err2) {
                    //delete the transaction
                    err2 = executeSqliteOrder("delete from doctransaction where id=" % currentTransactionString);
                }

                if (err2) err.addError(err2.getReturnCode(), err2.getMessage());
            } else {
                //For better performance, events are submitted only for the first recursive undo
                if (m_inundoRedoTransaction <= 1) {
                    //Emit modification events
                    QStringList tablesRefreshed;
                    foreach(const QString & table, listModifiedTables) {
                        Q_EMIT tableModified(table, getCurrentTransaction());
                        tablesRefreshed.push_back(table);
                    }
                    Q_EMIT tableModified("doctransaction", getCurrentTransaction());
                    Q_EMIT tableModified("doctransactionitem", getCurrentTransaction());

                    //WARNING: list is modified during treatement
                    for (int i = 0; !err && i < listModifiedTables.count(); ++i) {
                        QString table = listModifiedTables[i];
                        QStringList toAdd = getImpactedViews(table);
                        int nbToAdd = toAdd.count();
                        for (int j = 0; !err &&  j < nbToAdd; ++j) {
                            QString toAddTable = toAdd.at(j);
                            if (!listModifiedTables.contains(toAddTable)) {
                                //Compute materialized view of modified table
                                if (getCachedValue("SKG_REFRESH_VIEW") == "Y") err = computeMaterializedViews(toAddTable);
                                listModifiedTables.push_back(toAddTable);
                            }
                        }
                    }

                    //Emit events
                    for (int i = tablesRefreshed.count(); i < listModifiedTables.count(); ++i) {
                        Q_EMIT tableModified(listModifiedTables[i], 0);
                    }

                    Q_EMIT transactionSuccessfullyEnded(getCurrentTransaction());
                }
            }

            //Remove temporary transaction if needed
            if (!err) err = executeSqliteOrder("delete from doctransaction where t_name LIKE '#INTERNAL#%';");

            m_currentTransaction = 0;

            //clean cache
            m_cache.clear();

            if (qApp->type() != QApplication::Tty) QApplication::restoreOverrideCursor();
        }
    }
    return err;
}

SKGError SKGDocument::removeAllTransactions()
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::removeAllTransactions", err);
    //Check if a transaction is still opened
    err = checkExistingTransaction();
    if (!err) {
        //A transaction is still opened
        err.setReturnCode(ERR_ABORT);
        err.setMessage(i18nc("Something went wrong with SQL transactions", "Remove of transactions is forbidden inside a transaction"));

    } else {
        err = SKGDocument::beginTransaction("#INTERNAL#");
        if (!err) err = executeSqliteOrder("delete from doctransaction");
        if (!err) err = endTransaction(true);
        \
        else  endTransaction(false);

        //Force the save
        m_lastSavedTransaction = -1;
    }
    return err;
}

SKGError SKGDocument::computeMaterializedViews(const QString& iTable)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews", err);

    //Compute additional where clause
    QStringList tables;
    if (m_MaterializedViews.contains(iTable)) {
        tables = m_MaterializedViews[iTable];
    } else {
        QString wc;
        if (!iTable.isEmpty()) {
            QString t = iTable;
            if (t.startsWith(QLatin1String("v_"))) t.replace(0, 2, "vm_");
            wc = " AND name='" % t % '\'';
        }

        //Get list of materialized table
        err = getDistinctValues("sqlite_master", "name", "type='table' AND name LIKE 'vm_%' " % wc, tables);
        m_MaterializedViews[iTable] = tables;
    }

    //Refresh tables
    int nb = tables.count();
    for (int i = 0; !err && i < nb; ++i) {
        QString table = tables.at(i);
        QString view = table;
        view.replace(0, 3, "v_");

        //Remove previous table
        {
            SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews-drop-" % table, err);
            err = executeSqliteOrder("DROP TABLE IF EXISTS " % table);
        }
        {
            //Recreate table
            SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews-create-" % table, err);
            if (!err) err = executeSqliteOrder("CREATE TABLE " % table % " AS SELECT * FROM " % view);
        }
    }

    return err;
}

SKGError SKGDocument::sendMessage(const QString& iMessage, bool iPopup)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::sendMessage", err);
    //Associate message with transaction
    if (!checkExistingTransaction()) {
        SKGObjectBase msg(this, "doctransactionmsg");
        err = msg.setAttribute("rd_doctransaction_id", SKGServices::intToString(getCurrentTransaction()));
        if (!err) err = msg.setAttribute("t_message", iMessage);
        if (!err) err = msg.setAttribute("t_popup", iPopup ? "Y" : "N");
        if (!err) err = msg.save();
    } else {
        //Addition message in global variable in case of no transaction opened
        if (iPopup) m_unTransactionnalMessages.push_back(iMessage);
    }
    return err;
}

SKGError SKGDocument::getMessages(int iIdTransaction, QStringList& oMessages, bool iAll)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::getMessages", err);
    oMessages = m_unTransactionnalMessages;
    m_unTransactionnalMessages.clear();

    SKGStringListList listTmp;
    err = executeSelectSqliteOrder(
              QString("SELECT t_message, t_popup FROM doctransactionmsg WHERE ") %
              (iAll ? "t_popup IS NOT NULL" : "t_popup='Y'") %
              " AND rd_doctransaction_id=" %
              SKGServices::intToString(iIdTransaction) %
              " ORDER BY id ASC",
              listTmp);
    int nb = listTmp.count();
    for (int i = 1; !err && i < nb ; ++i) {
        QString msg = listTmp.at(i).at(0);
        if (!oMessages.contains(msg)) oMessages.push_back(msg);
    }
    return err;
}

SKGError SKGDocument::getModifications(int iIdTransaction, SKGObjectModificationList& oModifications)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::getModifications", err);
    oModifications.clear();

    SKGStringListList listTmp;
    err = executeSelectSqliteOrder(
              "SELECT i_object_id,t_object_table,t_action FROM doctransactionitem WHERE rd_doctransaction_id=" %
              SKGServices::intToString(iIdTransaction) %
              " ORDER BY id ASC",
              listTmp);
    int nb = listTmp.count();
    for (int i = 1; !err && i < nb ; ++i) {
        SKGObjectModification mod;
        mod.id = SKGServices::stringToInt(listTmp.at(i).at(0));
        mod.table = listTmp.at(i).at(1);
        QString type = listTmp.at(i).at(2);
        mod.type = (type == "D" ? I : (type == "I" ? D : U)); //Normal because in database we have to sql order to go back.
        mod.uuid = listTmp.at(i).at(0) % '-' % mod.table;

        oModifications.push_back(mod);
    }
    return err;
}

QStringList SKGDocument::getImpactedViews(const QString& iTable)
{
    SKGTRACEIN(10, "SKGDocument::getImpactedViews");
    if (m_ImpactedViews.count() == 0) {
        //Get list of tables and views
        QStringList tables;
        SKGStringListList result;
        executeSelectSqliteOrder("SELECT tbl_name FROM sqlite_master WHERE tbl_name NOT LIKE '%_delete' AND type IN ('table', 'view')", result);
        int nb = result.count();
        for (int i = 1; i < nb; ++i) {
            tables.push_back(result.at(i).at(0));
        }

        //First computation
        executeSelectSqliteOrder("SELECT tbl_name, sql FROM sqlite_master WHERE tbl_name NOT LIKE '%_delete' AND type='view'", result);
        nb = result.count();
        for (int i = 1; i < nb; ++i) {
            QStringList line = result.at(i);
            QString name = line.at(0);
            QString sql = line.at(1);

            QStringList words = SKGServices::splitCSVLine(sql, ' ', false);
            words.push_back("parameters");
            int nbWords = words.count();
            for (int j = 0; j < nbWords; ++j) {
                QString word = words.at(j);
                if (word.startsWith(QLatin1String("vm_"))) word.replace(0, 3, "v_");
                if (word != name && tables.contains(word, Qt::CaseInsensitive)) {
                    QStringList l = m_ImpactedViews[word];
                    if (!l.contains(name)) l.push_back(name);
                    m_ImpactedViews[word] = l;
                }
            }
        }
    }
    return m_ImpactedViews[iTable];
}

SKGError SKGDocument::groupTransactions(int iFrom, int iTo)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::groupTransactions", err);

    ++m_inundoRedoTransaction; //It's a kind of undo redo

    //Check if a transaction is still opened
    err = checkExistingTransaction();
    if (!err) {
        //A transaction is still opened
        err.setReturnCode(ERR_ABORT);
        err.setMessage(i18nc("Something went wrong with SQL transactions", "Creation of a group of transactions is forbidden inside a transaction"));

    } else {
        int iidMaster = qMax(iFrom, iTo);
        QString smin = SKGServices::intToString(qMin(iFrom, iTo));
        QString smax = SKGServices::intToString(iidMaster);

        //Get transaction
        SKGStringListList transactions;
        err = executeSelectSqliteOrder(
                  QString("SELECT id, t_name, t_mode, i_parent FROM doctransaction WHERE id BETWEEN ") %
                  smin % " AND " %
                  smax % " ORDER BY id ASC",
                  transactions);

        //Check and get main parameter for the group
        int nb = transactions.count();
        QString transactionMode;
        QString communParent;
        QString name;
        for (int i = 1; !err && i < nb; ++i) { //We forget header
            QStringList transaction = transactions.at(i);
            QString mode = transaction.at(2);
            if (!name.isEmpty()) name += ',';
            name += transaction.at(1);

            if (!transactionMode.isEmpty() && mode != transactionMode)  err = SKGError(ERR_INVALIDARG, "Undo and Redo transactions cannot be grouped");
            else  transactionMode = mode;

            if (i == 1)  communParent = transaction.at(3);
        }


        if (!err) {
            //Group
            err = SKGDocument::beginTransaction("#INTERNAL#");
            if (!err) {
                //Group items
                if (!err)
                    err = executeSqliteOrder(
                              QString("UPDATE doctransactionitem set rd_doctransaction_id=") %
                              smax %
                              " where rd_doctransaction_id BETWEEN " %
                              smin % " AND " % smax);
                if (!err)
                    err = executeSqliteOrder(
                              QString("UPDATE doctransaction set i_parent=") %
                              communParent %
                              ", t_name='" % SKGServices::stringToSqlString(name) %
                              "' where id=" % smax);

                if (!err) err = executeSqliteOrder(
                                        QString("DELETE FROM doctransaction WHERE id BETWEEN ") %
                                        smin % " AND " % SKGServices::intToString(qMax(iFrom, iTo) - 1));
            }

            if (!err) err = endTransaction(true);
            \
            else  endTransaction(false);
        }
    }

    --m_inundoRedoTransaction;
    return err;
}

SKGError SKGDocument::undoRedoTransaction(const UndoRedoMode& iMode)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::undoRedoTransaction", err);
    //Check if a transaction is still opened
    err = checkExistingTransaction();
    if (!err) {
        //A transaction is still opened
        err.setReturnCode(ERR_ABORT);
        err.setMessage(i18nc("Something went wrong with SQL transactions", "Undo / Redo is forbidden inside a transaction"));

    } else {
        if (iMode == SKGDocument::UNDOLASTSAVE) {
            //Create group
            SKGStringListList transactions;
            err = executeSelectSqliteOrder(
                      "SELECT id, t_savestep FROM doctransaction WHERE t_mode='U' ORDER BY id DESC",
                      transactions);
            int nb = transactions.count();
            int min = 0;
            int max = 0;
            for (int i = 1; !err && i < nb; ++i) {
                QStringList transaction = transactions.at(i);
                if (i == 1) {
                    max = SKGServices::stringToInt(transaction.at(0));
                }
                if (i != 1 && transaction.at(1) == "Y") {
                    break;
                }
                min = SKGServices::stringToInt(transaction.at(0));
            }
            if (min == 0) min = max;
            if (!err && min != max && min != 0) err = groupTransactions(min, max);
        } else {
            err = SKGError(); //To ignore error generated by checkExistingTransaction.
        }

        //Get ID of the transaction to undo
        if (!err) {
            QString name;
            bool saveStep = false;
            QDateTime date;
            bool refreshViews;
            int id = getTransactionToProcess(iMode, &name, &saveStep, &date, &refreshViews);
            if (id == 0) {
                //No transaction found ==> generate an error
                err = SKGError(ERR_INVALIDARG, "No transaction found. Undo / Redo impossible.");
            } else {

                //Undo transaction
                SKGTRACEL(5) << "Undoing transaction [" << id << "]- [" << name << "]..." << endl;
                SKGStringListList listSqlOrder;
                err = executeSelectSqliteOrder(
                          "SELECT t_sqlorder FROM doctransactionitem WHERE rd_doctransaction_id=" %
                          SKGServices::intToString(id) %
                          " ORDER BY id DESC",
                          listSqlOrder);
                if (!err) {
                    int nb = listSqlOrder.count();
                    err = SKGDocument::beginTransaction(name, nb + 3, date, refreshViews);
                    if (!err) {
                        ++m_inundoRedoTransaction; //Because we will be in a undo/redo transaction
                        //Normal the first element is ignored because it is the header
                        for (int i = 1; !err && i < nb ; ++i) {
                            err = executeSqliteOrder(listSqlOrder.at(i).at(0));

                            if (!err) err = stepForward(i); //Undo / redo are not cancelable
                        }

                        if (!err) {
                            //Set the NEW transaction in redo mode
                            int lastredo = getTransactionToProcess((iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE  ? SKGDocument::REDO : SKGDocument::UNDO));
                            int newredo = getTransactionToProcess(iMode);
                            if (!err)
                                err = executeSqliteOrder(
                                          QString("UPDATE doctransaction set t_mode=") %
                                          (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? "'R'" : "'U'") %
                                          ", i_parent=" %
                                          SKGServices::intToString(lastredo) %
                                          " where id=" % SKGServices::intToString(newredo));
                            if (!err) err = stepForward(nb);

                            //Move messages from previous transaction to new one
                            if (!err)
                                err = executeSqliteOrder(
                                          "UPDATE doctransactionmsg set rd_doctransaction_id=" %
                                          SKGServices::intToString(getCurrentTransaction()) %
                                          " where rd_doctransaction_id=" %
                                          SKGServices::intToString(id));
                            if (!err) err = stepForward(nb + 1);

                            //delete treated transaction
                            if (!err)
                                err = executeSqliteOrder(
                                          "DELETE from doctransaction where id="
                                          % SKGServices::intToString(id));
                            if (!err) err = stepForward(nb + 2);

                            //Check that new transaction has exactly the same number of item
                            /* if (!err) {
                                     SKGStringListList listSqlOrder;
                                     err=executeSelectSqliteOrder(
                                                     "SELECT count(1) FROM doctransactionitem WHERE rd_doctransaction_id=" %
                                                     SKGServices::intToString(getCurrentTransaction()),
                                                     listSqlOrder);
                                     if (!err && SKGServices::stringToInt(listSqlOrder.at(1).at(0))!=nb-1) {
                                             err=SKGError(ERR_ABORT, i18nc("Error message", "Invalid number of item after undo/redo. Expected (%1) != Result (%2)",nb-1,listSqlOrder.at(1).at(0)));
                                     }
                             }*/

                            if (!err) err = stepForward(nb + 3);
                        }

                        if (!err) err = endTransaction(true);
                        \
                        else  endTransaction(false);
                        --m_inundoRedoTransaction; //We left the undo / redo transaction
                    }
                }
            }
        }
    }

    return err;
}

int SKGDocument::getDepthTransaction() const
{
    return m_nbStepForTransaction.size();
}

int SKGDocument::getNbTransaction(const UndoRedoMode& iMode) const
{
    SKGTRACEIN(10, "SKGDocument::getNbTransaction");
    int output = 0;
    if (getDatabase()) {
        QString sqlorder = "select count(1) from doctransaction where t_mode='";
        sqlorder += (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? "U" : "R");
        sqlorder += '\'';
        QSqlQuery query = getDatabase()->exec(sqlorder);
        if (query.next()) {
            output = query.value(0).toInt();
        }
    }
    return output;
}

int SKGDocument::getTransactionToProcess(const UndoRedoMode& iMode, QString* oName, bool* oSaveStep, QDateTime* oDate, bool* oRefreshViews) const
{
    SKGTRACEIN(10, "SKGDocument::getTransactionToProcess");
    //initialisation
    int output = 0;
    if (oName) *oName = "";
    if (getDatabase()) {
        QString sqlorder = "select A.id , A.t_name, A.t_savestep, A.d_date, A.t_refreshviews from doctransaction A where "
                           "NOT EXISTS(select 1 from doctransaction B where B.i_parent=A.id) "
                           "and A.t_mode='";
        sqlorder += (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? "U" : "R");
        sqlorder += '\'';
        QSqlQuery query = getDatabase()->exec(sqlorder);
        if (query.next()) {
            output = query.value(0).toInt();
            if (oName != NULL) *oName = query.value(1).toString();
            if (oSaveStep != NULL) *oSaveStep = (query.value(2).toString() == "Y");
            if (oDate != NULL) *oDate = SKGServices::stringToTime(query.value(3).toString());
            if (oRefreshViews != NULL) *oRefreshViews = (query.value(4).toString() == "Y");
        }
    }
    return output;
}

int SKGDocument::getCurrentTransaction() const
{
    SKGTRACEIN(10, "SKGDocument::getCurrentTransaction");
    return m_currentTransaction;
}

SKGError SKGDocument::changePassword(const QString& newPassword)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::changePassword", err);
    err = setParameter("SKG_PASSWORD", newPassword);
    if (!err) err = sendMessage(newPassword.isEmpty() ? i18nc("Inform the user that the password protection was removed", "The document password has been removed.") :
                                    i18nc("Inform the user that the password was successfully changed", "The document password has been modified."));
    return err;
}

SKGError SKGDocument::setLanguage(const QString& iLanguage)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::setLanguage", err);
    QString previousLanguage = getParameter("SKG_LANGUAGE");
    if (previousLanguage != iLanguage) {
        //Save language into the document
        if (!err) err = beginTransaction("#INTERNAL#", 0, QDateTime::currentDateTime(), false);
        if (!err) err = setParameter("SKG_LANGUAGE", iLanguage);

        //Migrate view for new language
        if (!err) err = refreshViewsIndexesAndTriggers();

        //close temporary transaction
        if (!err) err = endTransaction(true);
        \
        else  endTransaction(false);
    }
    return err;
}

SKGError SKGDocument::initialize()
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::initialize", err);
    err = load("", "");
    return err;
}

SKGError SKGDocument::load(const QString& name, const QString& password, bool restoreTmpFile)
{
    //close previous document
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::load", err);
    SKGTRACEL(10) << "Input parameter [name]=[" << name << ']' << endl;

    m_lastSavedTransaction = -1; //To avoid double event emission
    err = close();
    if (!err) {
        if (!name.isEmpty()) {
            //File exist
            QFileInfo fi(name);
            m_modeReadOnly = !fi.permission(QFile::WriteUser);

            //Temporary file
            m_temporaryFile = SKGDocument::getTemporaryFile(name);
            if (!restoreTmpFile) {
                QFile::remove(m_temporaryFile); //Must must remove it to be able to copy
                err = SKGServices::cryptFile(name, m_temporaryFile, password, false, getDocumentHeader());
            } else {
                //BUG 249955: Check if password protected vvv
                //Temporary file will be loaded but first we must check if original document is password protected
                QString temporaryFile2 = m_temporaryFile % '2';
                err = SKGServices::cryptFile(name, temporaryFile2, password, false, getDocumentHeader());

                //Try an open to check if well descrypted
                if (!err) {
                    QSqlDatabase tryOpen(QSqlDatabase::addDatabase("QSQLITE", "tryOpen"));
                    tryOpen.setDatabaseName(temporaryFile2);
                    if (!tryOpen.open()) {
                        //Set error message
                        QSqlError sqlErr = tryOpen.lastError();
                        err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
                    }
                    if (!err) err = SKGServices::executeSqliteOrder(&tryOpen, "PRAGMA synchronous = OFF");
                }
                QSqlDatabase::removeDatabase("tryOpen");
                QFile::remove(temporaryFile2);

                //To avoid deletion of temporary file during next try
                if (err) m_temporaryFile = "";
                //BUG 249955: Check if password protected ^^^
            }

            //Create file database
            if (!err) {
                m_currentDatabase = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", m_databaseIdentifier));
                m_currentDatabase->setDatabaseName(m_temporaryFile);
                if (!m_currentDatabase->open()) {
                    //Set error message
                    QSqlError sqlErr = m_currentDatabase->lastError();
                    err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
                }

                m_directAccessDb = true;
                if (KUrl(name).isLocalFile()) m_currentFileName = name;
            }
        } else {
            //Temporary file
            m_temporaryFile = QDir::tempPath() % "/skg_" % QUuid::createUuid().toString() % ".skg";

            //Create memory database
            m_currentDatabase = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", m_databaseIdentifier));
            m_currentDatabase->setDatabaseName(":memory:");
            if (!m_currentDatabase->open()) {
                //Set error message
                QSqlError sqlErr = m_currentDatabase->lastError();
                err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
            }

            m_directAccessDb = false;
        }

        //Optimization
        QStringList optimization;
        optimization << "PRAGMA case_sensitive_like=true"
                     << "PRAGMA journal_mode=MEMORY"
                     << "PRAGMA temp_store=MEMORY"
                     << "PRAGMA locking_mode=EXCLUSIVE"
                     << "PRAGMA synchronous = OFF"
                     << "PRAGMA recursive_triggers=true"
                     ;
        if (!err) err = executeSqliteOrders(optimization);

        if (err) err.setReturnCode(ERR_ENCRYPTION);

        if (!m_directAccessDb) {
            //Create parameter and undo redo table
            /**
            * This constant is used to initialized the data model (table creation)
            */
            QStringList InitialDataModel;

            // ==================================================================
            //Table parameters
            InitialDataModel << "CREATE TABLE parameters "
                             "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                             "t_uuid_parent TEXT NOT NULL DEFAULT '',"
                             "t_name TEXT NOT NULL,"
                             "t_value TEXT NOT NULL DEFAULT '',"
                             "b_blob BLOB,"
                             "d_lastmodifdate DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                             "i_tmp INTEGER NOT NULL DEFAULT 0"
                             ")"

                             // ==================================================================
                             //Table node
                             << "CREATE TABLE node ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                             "t_name TEXT NOT NULL DEFAULT '' CHECK (t_name NOT LIKE '%" % OBJECTSEPARATOR % "%'),"
                             "t_fullname TEXT,"
                             "t_icon TEXT DEFAULT '',"
                             "f_sortorder FLOAT,"
                             "t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N')),"
                             "t_data TEXT,"
                             "rd_node_id INT CONSTRAINT fk_id REFERENCES node(id) ON DELETE CASCADE)"

                             // ==================================================================
                             //Table doctransaction
                             << "CREATE TABLE doctransaction ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                             "t_name TEXT NOT NULL,"
                             "t_mode VARCHAR(1) DEFAULT 'U' CHECK (t_mode IN ('U', 'R')),"
                             "d_date DATE NOT NULL,"
                             "t_savestep VARCHAR(1) DEFAULT 'N' CHECK (t_savestep IN ('Y', 'N')),"
                             "t_refreshviews VARCHAR(1) DEFAULT 'Y' CHECK (t_refreshviews IN ('Y', 'N')),"
                             "i_parent INTEGER)"

                             // ==================================================================
                             //Table doctransactionitem
                             << "CREATE TABLE doctransactionitem ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                             "rd_doctransaction_id INTEGER NOT NULL,"
                             "i_object_id INTEGER NOT NULL,"
                             "t_object_table TEXT NOT NULL,"
                             "t_action VARCHAR(1) DEFAULT 'I' CHECK (t_action IN ('I', 'U', 'D')),"
                             "t_sqlorder TEXT NOT NULL DEFAULT '')"

                             << "CREATE TABLE doctransactionmsg ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                             "rd_doctransaction_id INTEGER NOT NULL,"
                             "t_message TEXT NOT NULL DEFAULT '',"
                             "t_popup VARCHAR(1) DEFAULT 'Y' CHECK (t_popup IN ('Y', 'N')))";
            ;

            if (!err) err = executeSqliteOrders(InitialDataModel);
            if (!err) err = SKGDocument::refreshViewsIndexesAndTriggers();
        }
    }

    //migrate
    if (!err) {
        bool mig = false;
        err = migrate(mig);

        //To authorize manual repair of document in case of error during migration
        //the error is not caught if traces are activated
        if (err && SKGTraces::SKGLevelTrace) {
            err = sendMessage(i18nc("Popup message", "Warning: The migration failed but the document has been loaded without error because debug mode is activated"));
        }

        if (!err && mig && !name.isEmpty()) err = sendMessage(i18nc("The document has been upgraded to the latest Skrooge version format", "The document has been migrated"));
    }

    //Optimization
    if (!err) {
        m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
        executeSqliteOrder("ANALYZE");
    }

    //Creation undo/redo triggers
    if (!err) err = createUndoRedoTemporaryTriggers();

    if (err && !name.isEmpty()) {
        close();
    } else {
        //Send event
        m_uniqueIdentifier = QUuid::createUuid().toString();
        Q_EMIT tableModified("", 0);
    }

    return err;
}

bool SKGDocument::isReadOnly() const
{
    return m_modeReadOnly;
}

bool SKGDocument::isFileModified() const
{
    //Get last executed transaction
    int last = getTransactionToProcess(SKGDocument::UNDO);
    //  if (nbStepForTransaction.size()) --last;
    return (m_lastSavedTransaction != last);
}

void SKGDocument::setFileNotModified()
{
    m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
}

QString SKGDocument::getCurrentFileName() const
{
    return m_currentFileName;
}

SKGError SKGDocument::save()
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::save", err);
    if (m_currentFileName.isEmpty()) {
        err = SKGError(ERR_INVALIDARG, i18nc("Error message: Can not save a file if it has no name yet", "Save not authorized because the file name is not yet defined"));
    } else {
        //save
        err = saveAs(m_currentFileName, true);
    }
    return err;
}

SKGError SKGDocument::saveAs(const QString& name, bool overwrite)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::saveAs", err);
    SKGTRACEL(10) << "Input parameter [name]=[" << name << ']' << endl;

    //Check if a transaction is still opened
    err = checkExistingTransaction();
    if (!err) {
        //A transaction is still opened
        err.setReturnCode(ERR_ABORT);
        err.setMessage(i18nc("Cannot save the file while Skrooge is still performing an SQL transaction", "Save is forbidden if a transaction is still opened"));

    } else {
        err = SKGError();

        if (getParameter("SKG_UNDO_CLEAN_AFTER_SAVE") == "Y") {
            err = executeSqliteOrder("delete from doctransaction");
        }

        //No transaction opened ==> it's ok
        //We mark the last transaction as a save point
        if (!err) {
            err = executeSqliteOrder("update doctransaction set t_savestep='Y' where id in (select A.id from doctransaction A where "
                                     "NOT EXISTS(select 1 from doctransaction B where B.i_parent=A.id) "
                                     "and A.t_mode='U')");
        }
        Q_EMIT tableModified("doctransaction", 0);

        //Optimization
        if (!err) {
            err = executeSqliteOrder("VACUUM;");
            if (!err) {
                //Check if file already exist
                if (!overwrite && QFile(name).exists()) {
                    //Set error message
                    err.setReturnCode(ERR_INVALIDARG);
                    err.setMessage(i18nc("There is already a file with the same name", "File '%1' already exist", name));
                } else {
                    //Get backup file name
                    bool backupFileMustBeRemoved = false;
                    QString backupFileName = getBackupFile(name);
                    if (backupFileName.isEmpty()) {
                        backupFileName = name % ".tmp";
                        backupFileMustBeRemoved = true;
                    }

                    //Create backup file
                    QFile::remove(backupFileName);
                    if (QFile(name).exists() && !QFile(name).copy(backupFileName)) {
                        err = SKGError(ERR_FAIL, i18nc("Error message: Could not create a backup file", "Creation of backup file %1 failed", backupFileName));
                    }

                    //Save database
                    if (!err) {
                        QFile::remove(name);

                        //To be sure that db is flushed
                        if (!err) err = executeSqliteOrder("PRAGMA synchronous = FULL");

                        //Copy memory to tmp db
                        if (!m_directAccessDb && !err) {
                            QFile::remove(m_temporaryFile);
                            QSqlDatabase* fileDb = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", m_databaseIdentifier % "_tmp"));
                            fileDb->setDatabaseName(m_temporaryFile);
                            if (!fileDb->open()) {
                                //Set error message
                                QSqlError sqlErr = fileDb->lastError();
                                err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
                            } else {
                                if (!err) err = SKGServices::copySqliteDatabase(fileDb, m_currentDatabase, false);
                            }

                            fileDb->close();
                            delete fileDb;
                            QSqlDatabase::removeDatabase(m_databaseIdentifier % "_tmp");
                        }
                        if (!err) err = SKGServices::cryptFile(m_temporaryFile, name, getParameter("SKG_PASSWORD"), true, getDocumentHeader());
                        if (!m_directAccessDb) QFile(m_temporaryFile).remove();

                        //For performances
                        if (!err) err = executeSqliteOrder("PRAGMA synchronous = OFF");

                        //Restore backup in case of failure
                        if (err) {
                            QFile::remove(name);
                            QFile(backupFileName).rename(name);
                        }
                    }

                    if (backupFileMustBeRemoved) QFile::remove(backupFileName);

                    if (!err) {
                        //The document is not modified
                        QString oldtemporaryFile = m_temporaryFile;
                        m_currentFileName = name;
                        m_modeReadOnly = false;
                        m_temporaryFile = getTemporaryFile(m_currentFileName);
                        if (oldtemporaryFile != m_temporaryFile) QFile(oldtemporaryFile).rename(m_temporaryFile);
                        m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
                    }
                }
            }
        }
    }
    return err;
}

SKGError SKGDocument::close()
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::close", err);
    if (getDatabase() != NULL) {
        getDatabase()->close();
        delete m_currentDatabase; //Delete handler to avoid warning and memory leak
        QSqlDatabase::removeDatabase(m_databaseIdentifier);
    }

    if (!m_temporaryFile.isEmpty()) {
        QFile(m_temporaryFile).remove();
        m_temporaryFile = "";
    }

    //Emit events ?
    bool emitEvent = (m_lastSavedTransaction != -1);

    //Init fields
    m_currentDatabase = NULL;
    m_currentFileName = "";
    m_lastSavedTransaction = 0;
    m_nbStepForTransaction.clear();
    m_posStepForTransaction.clear();
    m_nameForTransaction.clear();

    //Send event
    if (emitEvent && qApp && !qApp->closingDown()) {
        Q_EMIT tableModified("", 0);
        Q_EMIT transactionSuccessfullyEnded(0);
    }

    return err;
}

SKGError SKGDocument::dropViewsAndIndexes(const QStringList& iTables) const
{
    SKGError err;
    //Drop all views
    SKGStringListList list;
    err = executeSelectSqliteOrder("SELECT tbl_name, name, type FROM sqlite_master WHERE type IN ('view','index')", list);
    int nb = list.count();
    for (int i = 0; !err && i < nb; ++i) {
        QString name = list.at(i).at(1);
        QString table = SKGServices::getRealTable(list.at(i).at(0));
        QString type = list.at(i).at(2);
        if (iTables.contains(table)) {
            QString sql = "DROP " % type % " IF EXISTS " % name;
            err = this->executeSqliteOrder(sql);
        }
    }
    return err;
}

SKGError SKGDocument::refreshViewsIndexesAndTriggers()
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::refreshViewsIndexesAndTriggers", err);
    /**
    * This constant is used to initialized the data model (trigger creation)
    */
    QString rd_node_id;
    QStringList attsOfNodes;
    err = getAttributesList("node", attsOfNodes);
    rd_node_id = (attsOfNodes.contains("rd_node_id") ? "rd_node_id" : "r_node_id");

    QStringList InitialDataModelTrigger;
    InitialDataModelTrigger
    DELETECASCADEPARAMETER("parameters")
    DELETECASCADEPARAMETER("node")

    //Compute fullname
            << "DROP TRIGGER IF EXISTS cpt_node_fullname3"
            /*<< "CREATE TRIGGER cpt_node_fullname1 " //This trigger must be the first
            "AFTER UPDATE OF t_fullname ON node BEGIN "
            "UPDATE node SET t_name=t_name WHERE rd_node_id=new.id;"
            "END"*/

            << "DROP TRIGGER IF EXISTS cpt_node_fullname1"
            << "CREATE TRIGGER cpt_node_fullname1 "
            "AFTER INSERT ON node BEGIN "
            "UPDATE node SET t_fullname="
            "CASE WHEN new.rd_node_id IS NULL OR new." % rd_node_id % "='' OR new." % rd_node_id % "=0 THEN new.t_name ELSE (SELECT c.t_fullname from node c where c.id=new." % rd_node_id % ")||'" % OBJECTSEPARATOR % "'||new.t_name END "
            "WHERE id=new.id;"
            "END"

            << "DROP TRIGGER IF EXISTS cpt_node_fullname2"
            << "CREATE TRIGGER cpt_node_fullname2 "
            "AFTER UPDATE OF t_name, " % rd_node_id % " ON node BEGIN "
            "UPDATE node SET t_fullname="
            "CASE WHEN new." % rd_node_id % " IS NULL OR new." % rd_node_id % "='' OR new." % rd_node_id % "=0 THEN new.t_name ELSE (SELECT c.t_fullname from node c where c.id=new." % rd_node_id % ")||'" % OBJECTSEPARATOR % "'||new.t_name END "
            "WHERE id=new.id;"
            "UPDATE node SET t_name=t_name WHERE " % rd_node_id % "=new.id;"
            "END"

            << "DROP TRIGGER IF EXISTS fkdc_node_parent_id_node_id"
            ;

    /**
    * This constant is used to initialized the data model (index creation)
    */
    QStringList InitialDataModelIndex;
    InitialDataModelIndex << "CREATE UNIQUE INDEX uidx_parameters_uuid_parent_name ON parameters (t_uuid_parent, t_name)"

                          << "CREATE UNIQUE INDEX uidx_node_parent_id_name ON node(t_name," % rd_node_id % ")"
                          << "CREATE INDEX idx_node_fullname ON node(t_fullname)"

                          << "CREATE INDEX idx_doctransaction_parent ON doctransaction (i_parent)"
                          << "CREATE INDEX idx_doctransactionitem_i_object_id ON doctransactionitem (i_object_id)"
                          << "CREATE INDEX idx_doctransactionitem_t_object_table ON doctransactionitem (t_object_table)"
                          << "CREATE INDEX idx_doctransactionitem_t_action ON doctransactionitem (t_action)"
                          << "CREATE INDEX idx_doctransactionitem_rd_doctransaction_id ON doctransactionitem (rd_doctransaction_id)"
                          << "CREATE INDEX idx_doctransactionitem_optimization ON doctransactionitem (rd_doctransaction_id, i_object_id, t_object_table, t_action, id)"
                          ;

    /**
    * This constant is used to initialized the data model (view creation)
    */
    QStringList InitialDataModelView;
    InitialDataModelView << "CREATE VIEW  v_node AS SELECT * from node"
                         << "CREATE VIEW v_node_displayname AS SELECT *, t_fullname AS t_displayname from node"
                         << "CREATE VIEW v_parameters_displayname AS SELECT *, t_name AS t_displayname from parameters"
                         ;

    QStringList tables;
    tables << "node" << "parameters" << "doctransactionitem" << "doctransaction";
    if (!err) err = dropViewsAndIndexes(tables);
    if (!err) err = executeSqliteOrders(InitialDataModelIndex);
    if (!err) err = executeSqliteOrders(InitialDataModelView);
    if (!err) err = executeSqliteOrders(InitialDataModelTrigger);
    if (!err) {
        //Refresh dynamic triggers
        QRegExp rx_rd("rd_([^_]+)_([^_]+).*");
        QRegExp rx_rc("rc_([^_]+)_([^_]+).*");
        QRegExp rx_r("r_([^_]+)_([^_]+).*");
        QStringList tables;
        err = this->getDistinctValues("sqlite_master", "name", "type='table'", tables);
        int nb = tables.count();
        for (int i = 0; !err && i < nb; ++i) {
            QString table = tables[i];
            SKGStringListList attributes;
            err = executeSelectSqliteOrder("PRAGMA table_info(" % table % ");", attributes);
            int nb2 = attributes.count();
            for (int j = 1; !err && j < nb2; ++j) { //Header is ignored
                QString att = attributes.at(j).at(1);
                if (rx_rd.indexIn(att) != -1) {
                    //Get parameters
                    QString tab2 = rx_rd.cap(1);
                    QString att2 = rx_rd.cap(2);
                    QStringList sqlOrders;
                    sqlOrders FOREIGNCONSTRAINTCASCADE(tab2, att2, table, att);
                    err = executeSqliteOrders(sqlOrders);
                } else if (rx_rc.indexIn(att) != -1) {
                    //Get parameters
                    QString tab2 = rx_rc.cap(1);
                    QString att2 = rx_rc.cap(2);
                    QStringList sqlOrders;
                    sqlOrders FOREIGNCONSTRAINT(tab2, att2, table, att);
                    err = executeSqliteOrders(sqlOrders);
                } else if (rx_r.indexIn(att) != -1) {
                    //Get parameters
                    QString tab2 = rx_r.cap(1);
                    QString att2 = rx_r.cap(2);
                    QStringList sqlOrders;
                    sqlOrders FOREIGNCONSTRAINTUPDATE(tab2, att2, table, att);
                    err = executeSqliteOrders(sqlOrders);
                    //The following sql order error is not caught because this repair order is not mandatory
                    executeSqliteOrder("UPDATE " % table % " SET " % att % "=0 WHERE " % att % "!=0 AND " % att % " NOT IN (SELECT DISTINCT(" % att2 % ") FROM " % tab2 % ')');
                }
            }
        }
    }

    if (!err) err = executeSqliteOrder("ANALYZE");

    return err;
}

SKGError SKGDocument::migrate(bool& oMigrationDone)
{
    SKGError err;
    SKGTRACEINRC(5, "SKGDocument::migrate", err);
    oMigrationDone = false;

    {
        SKGBEGINPROGRESSTRANSACTION(*this, "#INTERNAL#" % i18nc("Progression step", "Migrate document"), err, 3);
        if (getParameter("SKG_UNDO_MAX_DEPTH").isEmpty()) {
            err = setParameter("SKG_UNDO_MAX_DEPTH", SKGServices::intToString(SKG_UNDO_MAX_DEPTH));
        }

        if (getParameter("SKG_UNDO_CLEAN_AFTER_SAVE").isEmpty()) {
            err = setParameter("SKG_UNDO_CLEAN_AFTER_SAVE", "N");
        }

        QString version = getParameter("SKG_DB_VERSION");
        QString initialversion = version;
        QString lastversion = "1.4";

        if (!err && version.isEmpty()) {
            //First creation
            SKGTRACEL(10) << "Migration from 0 to 1.4" << endl;

            //Set new version
            version = lastversion;
            if (!err) err = setParameter("SKG_DB_VERSION", version);

            //Set sqlite creation version
            SKGStringListList listTmp;
            if (!err) err = executeSelectSqliteOrder("select sqlite_version()", listTmp);
            if (!err && listTmp.count() == 2) err = setParameter("SKG_SQLITE_CREATION_VERSION", listTmp.at(1).at(0));
            oMigrationDone = true;
        }

        if (!err && SKGServices::stringToDouble(version) > SKGServices::stringToDouble(lastversion)) {
            err = SKGError(ERR_ABORT, i18nc("Error message", "Impossible to load a document generated by a more recent version"));
        }

        {
            //Migration steps
            if (!err && version == "0.1") {
                //Migration from version 0.1 to 0.2
                SKGTRACEL(10) << "Migration from 0.1 to 0.2" << endl;

                // ==================================================================
                //Table doctransactionmsg
                QStringList sqlOrders;
                sqlOrders << "DROP TABLE IF EXISTS doctransactionmsg"
                          << "CREATE TABLE doctransactionmsg ("
                          "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                          "rd_doctransaction_id INTEGER NOT NULL,"
                          "t_message TEXT NOT NULL DEFAULT '')";
                err = executeSqliteOrders(sqlOrders);

                //Set new version
                version = "0.2";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.2") {
                //Migration from version 0.2 to 0.3
                SKGTRACEL(10) << "Migration from 0.2 to 0.3" << endl;

                err = executeSqliteOrder("UPDATE node set f_sortorder=id");

                //Set new version
                version = "0.3";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.3") {
                //Migration from version 0.3 to 0.4
                SKGTRACEL(10) << "Migration from 0.3 to 0.4" << endl;

                err = executeSqliteOrder("ALTER TABLE node ADD COLUMN t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N'))");
                if (!err) err = executeSqliteOrder("UPDATE node set t_autostart='Y' where t_name='" % i18nc("Verb, automatically load when the application is started", "autostart") % '\'');

                //Set new version
                version = "0.4";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.4") {
                //Migration from version 0.4 to 0.5
                SKGTRACEL(10) << "Migration from 0.4 to 0.5" << endl;

                err = executeSqliteOrder("ALTER TABLE doctransactionmsg ADD COLUMN t_popup VARCHAR(1) DEFAULT 'Y' CHECK (t_popup IN ('Y', 'N'))");

                //Set new version
                version = "0.5";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.5") {
                //Migration from version 0.5 to 0.6
                SKGTRACEL(10) << "Migration from 0.5 to 0.6" << endl;

                err = executeSqliteOrder("UPDATE node set t_autostart='N' where t_autostart NOT IN ('Y', 'N')");

                //Set new version
                version = "0.6";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.6") {
                //Migration from version 0.6 to 0.7
                SKGTRACEL(10) << "Migration from 0.6 to 0.7" << endl;

                err = executeSqliteOrder("ALTER TABLE parameters ADD COLUMN b_blob BLOB");

                //Set new version
                version = "0.7";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.7") {
                //Migration from version 0.7 to 0.8
                SKGTRACEL(10) << "Migration from 0.7 to 0.8" << endl;

                err = executeSqliteOrder("UPDATE parameters set t_name='SKG_LANGUAGE' where t_name='SKGLANGUAGE'");

                //Set new version
                version = "0.8";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.8") {
                SKGTRACEL(10) << "Migration from 0.8 to 0.9" << endl;

                QStringList sql;
                sql << "ALTER TABLE parameters ADD COLUMN i_tmp INTEGER NOT NULL DEFAULT 0"
                    << "UPDATE parameters set i_tmp=0"
                    ;

                err = executeSqliteOrders(sql);

                //Set new version
                version = "0.9";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "0.9") {
                SKGTRACEL(10) << "Migration from 0.9 to 1.0" << endl;

                err = SKGDocument::setParameter("SKG_UNIQUE_ID", "");

                //Set new version
                version = "1.0";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "1.0") {
                //Migration from version 1.0 to 1.1
                SKGTRACEL(10) << "Migration from 1.0 to 1.1" << endl;

                err = executeSqliteOrder("ALTER TABLE node ADD COLUMN t_icon TEXT DEFAULT ''");
                if (!err) {
                    SKGStringListList result;
                    err = executeSelectSqliteOrder("SELECT id,t_data from node", result);
                    int nb = result.count();
                    for (int i = 1; !err && i < nb; ++i) {
                        QStringList line = result.at(i);
                        QString icon = "folder-bookmark";
                        QStringList data = SKGServices::splitCSVLine(line.at(1));
                        if (data.count() > 2) icon = data.at(2);
                        data.removeAt(2);
                        err = executeSqliteOrder("UPDATE node set t_icon='" % SKGServices::stringToSqlString(icon) %
                                                 "', t_data='" % SKGServices::stringToSqlString(SKGServices::stringsToCsv(data)) % "' where id=" % line.at(0));
                    }
                }

                //Set new version
                version = "1.1";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "1.1") {
                //Migration from version 1.1 to 1.2
                SKGTRACEL(10) << "Migration from 1.1 to 1.2" << endl;

                QStringList sql;
                sql << "ALTER TABLE doctransaction ADD COLUMN t_refreshviews VARCHAR(1) DEFAULT 'Y' CHECK (t_refreshviews IN ('Y', 'N'))"
                    << "UPDATE doctransaction set t_refreshviews='Y'"
                    ;

                err = executeSqliteOrders(sql);

                //Set new version
                version = "1.2";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "1.2") {
                //Migration from version 1.2 to 1.3
                SKGTRACEL(10) << "Migration from 1.2 to 1.3" << endl;

                err = SKGDocument::refreshViewsIndexesAndTriggers();

                QStringList sql;
                sql << "DELETE FROM node WHERE (r_node_id IS NULL OR r_node_id='') AND EXISTS (SELECT 1 FROM node n WHERE n.t_name=node.t_name AND r_node_id=0)"
                    << "UPDATE node SET t_name=t_name"
                    ;
                if (!err) err = executeSqliteOrders(sql);

                //Set new version
                version = "1.3";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }
            if (!err && version == "1.3") {
                //Migration from version 1.3 to 1.4
                SKGTRACEL(10) << "Migration from 1.3 to 1.4" << endl;

                QStringList sql;
                sql   << "CREATE TABLE node2 ("
                      "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                      "t_name TEXT NOT NULL DEFAULT '' CHECK (t_name NOT LIKE '%" % OBJECTSEPARATOR % "%'),"
                      "t_fullname TEXT,"
                      "t_icon TEXT DEFAULT '',"
                      "f_sortorder FLOAT,"
                      "t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N')),"
                      "t_data TEXT,"
                      "rd_node_id INT CONSTRAINT fk_id REFERENCES node(id) ON DELETE CASCADE)"

                      << "INSERT INTO node2 (id, t_name, t_fullname, t_icon, f_sortorder, t_autostart, t_data, rd_node_id) "
                      "SELECT id, t_name, t_fullname, t_icon, f_sortorder, t_autostart, t_data, r_node_id FROM node"

                      << "DROP TABLE IF EXISTS node"
                      << "ALTER TABLE node2 RENAME TO node"
                      ;

                err = executeSqliteOrders(sql);

                //Set new version
                version = "1.4";
                if (!err) err = SKGDocument::setParameter("SKG_DB_VERSION", version);
                oMigrationDone = true;
            }

        }
        if (!err) err = stepForward(1, i18nc("Progression step", "Refresh views"));

        //Set sqlite last version
        SKGStringListList listTmp;
        if (!err) err = executeSelectSqliteOrder("select sqlite_version()", listTmp);
        if (!err && listTmp.count() == 2) err = setParameter("SKG_SQLITE_LAST_VERSION", listTmp.at(1).at(0));

        //Refresh views
        if (!err)  err = refreshViewsIndexesAndTriggers();
        if (!err) err = stepForward(2, i18nc("Progression step", "Update materialized views"));

        //Refresh materialized views
        if (!err && oMigrationDone) err = computeMaterializedViews();
        if (!err) err = stepForward(3);

        if (err) err.addError(ERR_FAIL, i18nc("Error message: Could not perform database migration", "Database migration from version %1 to version %2 failed", initialversion, version));
    }

    return err;
}

SKGError SKGDocument::createUndoRedoTemporaryTriggers()
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::createUndoRedoTemporaryTriggers", err);

    //Create triggers
    QStringList tables;
    err = this->getTablesList(tables);
    int nbTables = tables.count();
    for (int i = 0; !err && i < nbTables; ++i) {
        //Get table name
        QString table = tables[i];

        //Do we have to treat this table
        if (!SKGListNotUndoable.contains("T." % table) && !table.startsWith(QLatin1String("vm_"))) {
            //YES
            //Get attributes name
            QStringList attributes;
            err = getAttributesList(table, attributes);

            //Build sqlorder for update and insert
            QString sqlorderForUpdate2;
            QString sqlorderForInsert1;
            QString sqlorderForInsert2;
            int nbAttributes = attributes.count();
            for (int j = 0; !err && j < nbAttributes; ++j) {
                //Get attribute
                QString att = attributes[j];

                //Do we have to treat this attribute
                if (!SKGListNotUndoable.contains("A." % table % '.' % att)) {

                    //Build for update
                    if (!sqlorderForUpdate2.isEmpty())
                        sqlorderForUpdate2 += ',';
                    sqlorderForUpdate2 += att % "='||quote(old." % att % ")||'";

                    //Build for insert part 1
                    if (!sqlorderForInsert1.isEmpty())
                        sqlorderForInsert1 += ',';
                    sqlorderForInsert1 += att;

                    //Build for insert part 2
                    if (!sqlorderForInsert2.isEmpty())
                        sqlorderForInsert2 += ',';
                    sqlorderForInsert2 += "'||quote(old." % att % ")||'";
                }
            }

            //Create specific triggers for the current transaction
            QStringList sqlOrders;
            //DROP DELETE trigger
            sqlOrders << "DROP TRIGGER IF EXISTS UR_" % table % "_IN"

                      //Create DELETE trigger
                      << "CREATE TEMP TRIGGER UR_" % table % "_IN "
                      "AFTER  INSERT ON " % table % " BEGIN "
                      "INSERT INTO doctransactionitem (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'DELETE FROM " % table %
                      " WHERE id='||new.id,new.id,'" % table % "','D');END"

                      //DROP UPDATE trigger
                      << "DROP TRIGGER IF EXISTS UR_" % table % "_UP"

                      //Create UPDATE trigger
                      << "CREATE TEMP TRIGGER UR_" % table % "_UP "
                      "AFTER UPDATE ON " % table % " BEGIN "
                      "INSERT INTO doctransactionitem  (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'UPDATE " % table %
                      " SET " % sqlorderForUpdate2 %
                      " WHERE id='||new.id,new.id,'" % table % "','U');END"

                      //DROP INSERT trigger
                      << "DROP TRIGGER IF EXISTS UR_" % table % "_DE"

                      //Create INSERT trigger
                      << "CREATE TEMP TRIGGER UR_" % table % "_DE "
                      "AFTER DELETE ON " % table %
                      " BEGIN "
                      "INSERT INTO doctransactionitem  (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'INSERT INTO " % table %
                      '(' % sqlorderForInsert1 % ") VALUES(" % sqlorderForInsert2 % ")',old.id,'" % table % "','I'); END";
            err = executeSqliteOrders(sqlOrders);
        }
    }
    return err;
}

QStringList SKGDocument::getParameters(const QString& iParentUUID, const QString& iWhereClause)
{
    SKGTRACEIN(10, "SKGDocument::getParameters");
    QStringList output;
    QString wc = "t_uuid_parent='" % SKGServices::stringToSqlString(iParentUUID) % '\'';
    if (!iWhereClause.isEmpty()) wc += " AND (" % iWhereClause % ')';
    this->getDistinctValues("parameters", "t_name", wc, output);
    return output;
}

QString SKGDocument::getParameter(const QString& iName, const QString& iParentUUID)
{
    SKGTRACEIN(10, "SKGDocument::getParameter");
    SKGTRACEL(10) << "Input parameter [iName]=[" << iName << ']' << endl;
    QString output;

    //Get parameter
    SKGObjectBase param;
    SKGError err = getObject("parameters", "t_name='" % SKGServices::stringToSqlString(iName) %
                             "' AND t_uuid_parent='" % SKGServices::stringToSqlString(iParentUUID) % '\'', param);
    if (!err) {
        output = param.getAttribute("t_value");
    }
    return output;
}

QVariant SKGDocument::getParameterBlob(const QString& iName, const QString& iParentUUID)
{
    SKGTRACEIN(10, "SKGDocument::getParameterBlob");
    SKGTRACEL(10) << "Input parameter [iName]=[" << iName << ']' << endl;
    QVariant output;

    QString sqlQuery = "SELECT b_blob FROM parameters WHERE t_name=? AND t_uuid_parent=?";
    QSqlQuery query(*getDatabase());
    query.prepare(sqlQuery);
    query.addBindValue(iName);
    query.addBindValue(iParentUUID);
    if (!query.exec()) {
        QSqlError sqlError = query.lastError();
        SKGTRACE << "WARNING: " << sqlQuery << endl;
        SKGTRACE << "         returns :" << sqlError.text() << endl;
    } else {
        if (query.next()) output = query.value(0);
    }

    return output;
}

SKGError SKGDocument::setParameter(const QString& iName, const QString& iValue, const QString& iFileName, const QString& iParentUUID, SKGPropertyObject* oObjectCreated)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::setParameter", err);
    SKGTRACEL(10) << "Input parameter [iName]    =[" << iName << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iValue]   =[" << iValue << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iFileName]=[" << iFileName << ']' << endl;
    QVariant blob;
    QString value = iValue;
    QFile file(iFileName);
    if (file.exists()) {
        QFileInfo fileInfo(iFileName);
        if (fileInfo.isDir()) {
            value = "file://" % iFileName;
        } else {
            //Open file
            if (!file.open(QIODevice::ReadOnly)) {
                err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not open a file", "Open file '%1' failed", iFileName));
            } else {
                QByteArray blob_bytes = file.readAll();
                if (!blob_bytes.count()) {
                    err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not open a file", "Open file '%1' failed", iFileName));
                } else {
                    blob = blob_bytes;
                    value = fileInfo.fileName();
                }

                //close file
                file.close();
            }
        }
    }

    if (!err) err = setParameter(iName, value, blob, iParentUUID, oObjectCreated);
    return err;
}

SKGError SKGDocument::setParameter(const QString& iName, const QString& iValue, const QVariant& iBlob, const QString& iParentUUID, SKGPropertyObject* oObjectCreated)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::setParameter", err);
    SKGTRACEL(10) << "Input parameter [iName]    =[" << iName << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iValue]   =[" << iValue << ']' << endl;

    SKGPropertyObject param(this);
    if (!err) err = param.setName(iName);
    if (!err) err = param.setValue(iValue);
    if (!err) err = param.setParentId(iParentUUID);
    if (!err) err = param.save();

    if (!err && !iBlob.isNull()) {
        err = param.load();
        if (!err) {
            //Set blob
            QString sqlQuery = "UPDATE parameters SET b_blob=? WHERE id=?";
            QSqlQuery query(*getDatabase());
            query.prepare(sqlQuery);
            query.addBindValue(iBlob);
            query.addBindValue(param.getID());
            if (!query.exec()) {
                QSqlError sqlError = query.lastError();
                QString msg = sqlQuery % ':' % sqlError.text();
                err = SKGError(SQLLITEERROR + sqlError.number(), msg);
            }
        }
    }
    if (!err && oObjectCreated != NULL) *oObjectCreated = param;

    return err;
}

SKGError SKGDocument::dump(int iMode)
{
    SKGError err;
    if (getDatabase()) {
        //dump parameters
        SKGTRACE << "=== START DUMP ===" << endl;
        if (iMode & DUMPSQLITE) {
            SKGTRACE << "=== DUMPSQLITE ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM sqlite_master order by type"));

            SKGTRACE << "=== DUMPSQLITE (TEMPORARY) ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM sqlite_temp_master order by type"));
        }

        if (iMode & DUMPPARAMETERS) {
            SKGTRACE << "=== DUMPPARAMETERS ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM parameters order by id"));
        }

        if (iMode & DUMPNODES) {
            SKGTRACE << "=== DUMPNODES ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM node order by id"));
        }

        if (iMode & DUMPTRANSACTIONS) {
            //dump transaction
            SKGTRACE << "=== DUMPTRANSACTIONS ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM doctransaction order by id"));

            //dump transaction
            SKGTRACE << "=== DUMPTRANSACTIONS (ITEMS) ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM doctransactionitem order by rd_doctransaction_id, id"));
        }
        SKGTRACE << "=== END DUMP ===" << endl;
    }
    return err;
}

QSqlDatabase* SKGDocument::getDatabase() const
{
    return m_currentDatabase;
}

SKGError SKGDocument::getConsolidatedView(const QString& iTable,
        const QString& iAsColumn,
        const QString& iAsRow,
        const QString& iAttribute,
        const QString& iOpAtt,
        const QString& iWhereClause,
        SKGStringListList& oTable,
        const QString& iMissingValue)
{
    SKGError err;
    SKGTRACEINRC(10, "SKGDocument::getConsolidatedView", err);
    SKGTRACEL(10) << "Input parameter [iTable]=[" << iTable << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iAsColumn]=[" << iAsColumn << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iAsRow]=[" << iAsRow << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iAttribute]=[" << iAttribute << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iOpAtt]=[" << iOpAtt << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iWhereClause]=[" << iWhereClause << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iMissingValue]=[" << iMissingValue << ']' << endl;

    //Mode
    int mode = 0;
    if (!iAsColumn.isEmpty()) mode += 1;
    if (!iAsRow.isEmpty()) mode += 2;

    oTable.clear();
    oTable.push_back(QStringList());

    QStringList* titles = (QStringList*) & (oTable.at(0));

    if (mode == 3) {
        titles->push_back(iAsRow % '/' % iAsColumn);
    } else {
        if (mode == 1) {
            titles->push_back(iAsColumn);

            QStringList sums;
            sums.push_back(i18nc("Noun, the numerical sum of a list of values", "Sum"));
            oTable.push_back(sums);
        } else {
            if (mode == 2) {
                titles->push_back(iAsRow);
                titles->push_back(i18nc("Noun, the numerical sum of a list of values", "Sum"));
            }
        }
    }

    //Create sqlorder
    QString asColumn = iAsColumn;
    if (asColumn.startsWith(QLatin1String("p_"))) {
        QString propertyName = asColumn.right(asColumn.length() - 2);
        asColumn = "(SELECT t_value FROM parameters WHERE t_uuid_parent=" % iTable % ".id||'-" % SKGServices::getRealTable(iTable) % "' AND t_name='" % propertyName % "')";
    }
    QString asRow = iAsRow;
    if (asRow.startsWith(QLatin1String("p_"))) {
        QString propertyName = asRow.right(asRow.length() - 2);
        asRow = "(SELECT t_value FROM parameters WHERE t_uuid_parent=" % iTable % ".id||'-" % SKGServices::getRealTable(iTable) % "' AND t_name='" % propertyName % "')";
    }

    QString att = asColumn;
    if (!att.isEmpty() && !asRow.isEmpty()) att += ',';
    att += asRow;

    QString sort = asRow;
    if (!sort.isEmpty() && !asColumn.isEmpty()) sort += ',';
    sort += asColumn;

    if (!att.isEmpty()) {
        QString sql = "SELECT " % att % ',' % iOpAtt % '(' % iAttribute % ") FROM " % iTable;
        if (!iWhereClause.isEmpty()) sql += " WHERE " % iWhereClause;
        if (!iOpAtt.isEmpty()) sql += " GROUP BY " % att;
        sql += " ORDER BY " % sort;

        QHash<QString, int> cols;
        QHash<QString, int> rows;

        SKGTRACEL(10) << "sqlorder=[" << sql << ']' << endl;
        SKGStringListList listTmp;
        {
            SKGTRACEINRC(10, "SKGDocument::getConsolidatedView-execute sql", err);
            err = executeSelectSqliteOrder(sql, listTmp);
        }
        int nb = listTmp.count();
        for (int i = 1; !err && i < nb; ++i) { //Title is ignored
            QStringList line = listTmp.at(i);
            int rowindex = -1;
            int colindex = -1;
            if (mode >= 2) {
                QString rowname = line.at(mode == 3 ? 1 : 0);

                if (!rows.contains(rowname)) {
                    QStringList r;
                    r.push_back(rowname);
                    int nbx = oTable[0].count();
                    for (int j = 1; j < nbx; ++j)
                        r.push_back(iMissingValue);

                    oTable.push_back(r);

                    rowindex = oTable.count() - 1;
                    rows.insert(rowname, rowindex);
                } else rowindex = rows[rowname];
            } else rowindex = 1;

            if (mode == 1 || mode == 3) {
                QString colname = line.at(0);

                if (!cols.contains(colname)) {
                    //Search better position of this column
                    colindex = -1;
                    {
                        QHashIterator<QString, int> cols_i(cols);
                        while (cols_i.hasNext()) {
                            cols_i.next();
                            if (colname > cols_i.key() && cols_i.value() > colindex) colindex = cols_i.value();
                        }
                    }
                    if (colindex == -1) colindex = 1;
                    else ++colindex;

                    int nbx = oTable.count();
                    for (int j = 0; j < nbx; ++j) {
                        if (j == 0) oTable[j].insert(colindex, colname);
                        else oTable[j].insert(colindex, iMissingValue);
                    }

                    {
                        QHash<QString, int> tmp;
                        QHashIterator<QString, int> cols_i(cols);
                        while (cols_i.hasNext()) {
                            cols_i.next();
                            tmp.insert(cols_i.key(), cols_i.value() + (cols_i.value() >= colindex ? 1 : 0));
                        }

                        cols = tmp;
                    }
                    cols.insert(colname, colindex);

                } else colindex = cols[colname];
            } else colindex = 1;

            QString sum = line.at(mode == 3 ? 2 : 1);

            oTable[rowindex][colindex] = sum;
        }

        IFSKGTRACEL(10) {
            QStringList dump = SKGServices::tableToDump(oTable, SKGServices::DUMP_TEXT);
            int nbl = dump.count();
            for (int i = 0; i < nbl; ++i) {
                SKGTRACE << dump[i] << endl;
            }
        }

        //Correction bug 205466 vvv
        //If some months or years are missing, we must add them.
        if (asColumn.startsWith(QLatin1String("d_"))) {
            SKGTRACEINRC(10, "SKGDocument::getConsolidatedView-date treatement", err);
            for (int c = 1; c < oTable[0].count() - 1; ++c) { //Dynamic size
                bool forecast = false;
                QString title = oTable.at(0).at(c);
                if (title.isEmpty()) title = "0000";

                if (title.endsWith(QLatin1String("999"))) {
                    title = title.left(title.count() - 3);
                    forecast = true;
                }
                QString nextTitle = oTable.at(0).at(c + 1);
                if (nextTitle.endsWith(QLatin1String("999"))) {
                    nextTitle = nextTitle.left(nextTitle.count() - 3);
                    forecast = true;
                }

                QString dateFormat = (asColumn == "d_date" ? "yyyy-MM-dd" : (asColumn == "d_DATEMONTH" ? "yyyy-MM" : (asColumn == "d_DATEQUARTER" ? "yyyy-QM" : (asColumn == "d_DATESEMESTER" ? "yyyy-SM" : (asColumn == "d_DATEWEEK" ? "yyyy-WM" : "yyyy")))));
                QDate nextExpected = QDate::fromString(title, dateFormat);
                QString nextExpectedString;
                if (asColumn == "d_DATEWEEK") {
                    /* TODO
                                      QStringList items=SKGServices::splitCSVLine(oTable.at(0).at(c),'-');
                                      nextExpected=QDate(SKGServices::stringToInt(items.at(0)), 1, 1);
                                      QString w=items.at(1);
                                      w.remove('W');
                                      nextExpected=nextExpected.addDays(7*SKGServices::stringToInt(w));
                                      QString newW=SKGServices::intToString(nextExpected.weekNumber());
                                      if(newW.count()==1) newW='0'+newW;
                                      */
                    nextExpectedString = nextTitle;
                } else if (asColumn == "d_DATEMONTH") {
                    nextExpected = nextExpected.addMonths(1);
                    nextExpectedString = nextExpected.toString(dateFormat);
                } else if (asColumn == "d_DATEQUARTER") {
                    nextExpected = nextExpected.addMonths(nextExpected.month() * 3 - nextExpected.month()); //convert quater in month
                    nextExpected = nextExpected.addMonths(3);
                    nextExpectedString = nextExpected.toString("yyyy-Q") % (nextExpected.month() <= 3 ? '1' : (nextExpected.month() <= 6 ? '2' : (nextExpected.month() <= 9 ? '3' : '4')));
                } else if (asColumn == "d_DATESEMESTER") {
                    nextExpected = nextExpected.addMonths(nextExpected.month() * 6 - nextExpected.month()); //convert semester in month
                    nextExpected = nextExpected.addMonths(6);
                    nextExpectedString = nextExpected.toString("yyyy-S") % (nextExpected.month() <= 6 ? '1' : '2');
                } else if (asColumn == "d_DATEYEAR") {
                    nextExpected = nextExpected.addYears(1);
                    nextExpectedString = nextExpected.toString(dateFormat);
                } else {
                    nextExpected = nextExpected.addDays(1);
                    nextExpectedString = nextExpected.toString(dateFormat);
                }
                if (title != "0000" && nextTitle != nextExpectedString && nextTitle != title) {
                    int colindex = c + 1;
                    if (forecast) nextExpectedString += "999";

                    int nbx = oTable.count();
                    oTable[0].insert(colindex, nextExpectedString);
                    for (int j = 1; j < nbx; ++j) {
                        oTable[j].insert(colindex, iMissingValue);
                    }
                }
            }
        }
        //Correction bug 205466 ^^^
    }

    return err;
}

QList<SKGDocument::SKGModelTemplate> SKGDocument::getDisplaySchemas(const QString& iRealTable) const
{
    QList<SKGDocument::SKGModelTemplate> listSchema;

    //Build schemas
    if (iRealTable == "doctransaction") {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "t_name;t_value;d_lastmodifdate;t_savestep";
        listSchema.push_back(def);

        SKGModelTemplate minimum;
        minimum.id = "minimum";
        minimum.name = i18nc("Noun, the minimum value of an item", "Minimum");
        minimum.icon = "";
        minimum.schema = "t_name;t_value;d_lastmodifdate|N;t_savestep|N";
        listSchema.push_back(minimum);
    } else if (iRealTable == "parameters") {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "t_name;t_value";
        listSchema.push_back(def);
    } else if (iRealTable == "node") {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "t_name";
        listSchema.push_back(def);
    }

    return listSchema;
}

QString SKGDocument::getDisplay(const QString& iString) const
{
    QString output = iString.toLower();

    if (output.endsWith(QLatin1String("t_name"))) output = i18nc("Noun, the name of an item", "Name");
    else if (output.endsWith(QLatin1String("d_date"))) output = i18nc("Noun, the date of an item", "Date");
    else if (output.endsWith(QLatin1String("t_savestep"))) output = i18nc("Verb, save a document", "Save");
    else if (output.endsWith(QLatin1String("t_value"))) output = i18nc("Noun, the value of an item", "Value");
    else if (output.endsWith(QLatin1String("d_lastmodifdate"))) output = i18nc("Noun, date of last modification", "Last modification");
    else if (output.startsWith(QLatin1String("p_")) || output.contains("p_")) {
        //This is a property
        int pos = iString.indexOf(".");
        if (pos != -1) output = iString.right(iString.count() - pos - 1);
        output = output.right(output.length() - 2);
    } else output = iString;
    return output;
}

QIcon SKGDocument::getIcon(const QString& iString) const
{
    QString output = iString.toLower();
    if (output.startsWith(QLatin1String("p_")) || output.contains("p_")) return KIcon("feed-subscribe");
    return QIcon();
}

QString SKGDocument::getRealAttribute(const QString& iString) const
{
    if (iString == iString.toLower()) return iString;
    return "";
}

SKGServices::AttributeType SKGDocument::getAttributeType(const QString& iAttributeName) const
{
    SKGServices::AttributeType output = SKGServices::TEXT;
    if (iAttributeName.startsWith(QLatin1String("d_"))) output = SKGServices::DATE;
    else if (iAttributeName.startsWith(QLatin1String("i_"))) output = SKGServices::INTEGER;
    else if (iAttributeName.startsWith(QLatin1String("rd_")) || iAttributeName.startsWith(QLatin1String("rc_")) || iAttributeName.startsWith(QLatin1String("r_"))) output = SKGServices::LINK;
    else if (iAttributeName.startsWith(QLatin1String("f_"))) output = SKGServices::FLOAT;
    else if (iAttributeName.startsWith(QLatin1String("b_"))) output = SKGServices::BLOB;
    else if (iAttributeName == "id") output = SKGServices::ID;

    return output;
}

SKGServices::SKGUnitInfo SKGDocument::getUnit(const QString& iPrefixInCache)
{
    SKGServices::SKGUnitInfo output;
    output.Name = getCachedValue(iPrefixInCache % "UnitCache");
    output.Symbol = getCachedValue(iPrefixInCache % "UnitSymbolCache");
    QString val = getCachedValue(iPrefixInCache % "UnitValueCache");
    if (!val.isEmpty()) output.Value = SKGServices::stringToDouble(val);
    else output.Value = 1;
    val = getCachedValue(iPrefixInCache % "UnitDecimalCache");
    if (!val.isEmpty()) output.NbDecimal = SKGServices::stringToInt(val);
    else output.NbDecimal = 2;

    return output;
}

QString SKGDocument::formatMoney(double iValue, SKGServices::SKGUnitInfo iUnit, bool iHtml) const
{
    KLocale* locale = KGlobal::locale();
    QString val = (locale ? locale->formatMoney(iValue / iUnit.Value, iUnit.Symbol, iUnit.NbDecimal) : SKGServices::doubleToString(round(iValue / iUnit.Value / 100) * 100) % ' ' % iUnit.Symbol);
    if (iHtml) {
        //Get std colors
        KColorScheme scheme(QPalette::Normal);
        QString negative = scheme.foreground(KColorScheme::NegativeText).color().name();
        QString neutral = scheme.foreground(KColorScheme::NormalText).color().name();

        //Return value
        return QString("<font color=\"") %
               (iValue < 0 ? negative : neutral) %
               "\">" %
               SKGServices::stringToHtml(val) %
               "</font>";
    }
    return val;
}

QString SKGDocument::formatPercentage(double iValue, bool iInvertColors) const
{
    KLocale* locale = KGlobal::locale();

    //Get std colors
    KColorScheme scheme(QPalette::Normal);
    QString negative = scheme.foreground(KColorScheme::NegativeText).color().name();
    QString positive = scheme.foreground(KColorScheme::PositiveText).color().name();

    //Return value
    QString p = (locale ? locale->formatMoney(iValue, "%", 2) : SKGServices::doubleToString(iValue) % " %");
    if (iValue > 0) p = '+' % p;
    if (p.count() > 10 || isnan(iValue) || isinf(iValue)) p = QChar(8734);
    return "<font color=\"" %
           QString((iValue < 0 && !iInvertColors) || (iValue >= 0 && iInvertColors) ? negative : positive) %
           "\">" % SKGServices::stringToHtml(p) %
           "</font>";
}

QString SKGDocument::getFileExtension() const
{
    return "skgc";
}

QString SKGDocument::getDocumentHeader() const
{
    return "";
}

void SKGDocument::addValueInCache(const QString& iKey, const QString& iValue)
{
    m_cache[iKey] = iValue;
}

QString SKGDocument::getCachedValue(const QString& iKey) const
{
    return m_cache[iKey];
}

void SKGDocument::setBackupParameters(const QString& iPrefix, const QString& iSuffix)
{
    m_backupPrefix = iPrefix;
    m_backupSuffix = iSuffix;
}

QString SKGDocument::getCurrentTemporaryFile()
{
    return m_temporaryFile;
}

QString SKGDocument::getTemporaryFile(const QString iFileName)
{
    QString output;
    QFileInfo fi(iFileName);
    QFileInfo di(fi.dir().path());
    if (!KUrl(iFileName).isLocalFile() || !di.permission(QFile::WriteUser)) output = QDir::tempPath();
    else output = fi.absolutePath();
    return output += "/." % fi.fileName() % ".wrk";
}

QString SKGDocument::getBackupFile(const QString iFileName)
{
    QString output;
    if (!m_backupPrefix.isEmpty() || !m_backupSuffix.isEmpty()) {
        QFileInfo fi(iFileName);
        output = fi.absolutePath() % '/' % m_backupPrefix % fi.fileName() % m_backupSuffix;
        output = output.replace("<DATE>", SKGServices::timeToString(QDateTime::currentDateTime()));
    }

    return output;
}

SKGError SKGDocument::getObjects(const QString& iTable, const QString& iWhereClause, SKGObjectBase::SKGListSKGObjectBase& oListObject) const
{
    SKGError err;

    //Initialisation
    oListObject.clear();

    //Execute sqlorder
    SKGStringListList result;
    err = executeSelectSqliteOrder(
              QString("SELECT * FROM " % iTable %
                      (!iWhereClause.isEmpty() ? QString(" WHERE " % iWhereClause) : "")),
              result);

    //Create output
    if (!err) {
        SKGStringListListIterator itrow = result.begin();
        QStringList columns = *(itrow);
        ++itrow;
        for (; !err && itrow != result.end(); ++itrow) {
            QStringList values = *(itrow);
            SKGObjectBase tmp((SKGDocument*) this, iTable);
            err = tmp.setAttributes(columns, values);
            oListObject.push_back(tmp);
        }
    }
    return err;
}

SKGError SKGDocument::existObjects(const QString& iTable, const QString& iWhereClause, bool& oExist) const
{
    SKGError err;

    //Initialisation
    oExist = false;

    //Execute sqlorder
    SKGStringListList result;
    err = executeSelectSqliteOrder(
              "SELECT EXISTS(SELECT 1 FROM " % iTable % " WHERE " %
              (!iWhereClause.isEmpty() ?  iWhereClause  : "1=1") % ')',
              result);

    //Create output
    if (!err) oExist = (result.at(1).at(0) == "1");
    return err;
}

SKGError SKGDocument::getNbObjects(const QString& iTable, const QString& iWhereClause, int& oNbObjects) const
{
    SKGError err;

    //Initialisation
    oNbObjects = 0;

    //Execute sqlorder
    SKGStringListList result;
    err = executeSelectSqliteOrder(
              QString("SELECT count(1) FROM " % iTable %
                      (!iWhereClause.isEmpty() ? QString(" WHERE " % iWhereClause) : "")),
              result);

    //Create output
    if (!err) oNbObjects = SKGServices::stringToInt(result.at(1).at(0));
    return err;
}

SKGError SKGDocument::getObject(const QString& iTable, const QString& iWhereClause, SKGObjectBase& oObject) const
{
    SKGObjectBase::SKGListSKGObjectBase temporaryResult;
    oObject.resetID();
    SKGError err = SKGDocument::getObjects(iTable, iWhereClause, temporaryResult);
    if (!err) {
        int size = temporaryResult.size();
        if (size > 1)  err = SKGError(ERR_INVALIDARG, i18nc("Error message: We expected only one object in the result, but got more", "More than one object returned in '%1' for '%2'", iTable, iWhereClause));
        else {
            if (size == 0)  err = SKGError(ERR_INVALIDARG, i18nc("Error message: We expected at least one object in the result, but got none", "No object returned in '%1' for '%2'", iTable, iWhereClause));
            else  oObject = *(temporaryResult.begin());
        }
    }
    return err;
}

SKGError SKGDocument::getObject(const QString& iTable, int iId, SKGObjectBase& oObject) const
{
    return getObject(iTable, "id=" % SKGServices::intToString(iId), oObject);
}

SKGError SKGDocument::getTablesList(QStringList& oResult) const
{
    return getDistinctValues("sqlite_master", "name",
                             "type='table' AND name NOT LIKE 'sqlite_%'",
                             oResult);
}

SKGError SKGDocument::getDistinctValues(const QString& iTable, const QString& iAttribute, const QString& iWhereClause, QStringList& oResult) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGServices::getDistinctValues", err);
    //initialisation
    oResult.clear();

    //Search
    SKGStringListList temporaryResult;
    err = executeSelectSqliteOrder(
              "SELECT DISTINCT " % iAttribute %
              " FROM " % iTable % " WHERE (" %
              (!iWhereClause.isEmpty() ? iWhereClause : "1=1") %
              ") ORDER BY " % iAttribute
              //Correction bug 202167 vvv
              % " COLLATE NOCASE"
              //Correction bug 202167 ^^^
              , temporaryResult);
    if (!err) {
        SKGStringListListIterator it = temporaryResult.begin();
        ++it; //to forget column name
        for (; it != temporaryResult.end(); ++it) {
            oResult.push_back(*(it->begin()));
        }
    }

    return err;
}

SKGError SKGDocument::getDistinctValues(const QString& iTable, const QString& iAttribute, QStringList& oResult) const
{
    return getDistinctValues(iTable, iAttribute,
                             iAttribute % " IS NOT NULL AND " % iAttribute % "!=''",
                             oResult);
}

SKGError SKGDocument::executeSqliteOrder(const QString& iSqlOrder, int* iLastId) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGServices::executeSqliteOrder", err);
    err = SKGServices::executeSqliteOrder(getDatabase(), iSqlOrder, iLastId);
    return err;
}

SKGError SKGDocument::executeSqliteOrders(const QStringList& iSqlOrders) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::executeSqliteOrders", err);
    err = SKGServices::executeSqliteOrders(getDatabase(), iSqlOrders);
    return err;
}

SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QTextStream* oStream, SKGServices::DumpMode iMode) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::dumpSelectSqliteOrder", err);
    err = SKGServices::dumpSelectSqliteOrder(getDatabase(), iSqlOrder, oStream, iMode);
    return err;
}

SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QString& oResult, SKGServices::DumpMode iMode) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::dumpSelectSqliteOrder", err);
    err = SKGServices::dumpSelectSqliteOrder(getDatabase(), iSqlOrder, oResult, iMode);
    return err;
}

SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QStringList& oResult, SKGServices::DumpMode iMode) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::dumpSelectSqliteOrder", err);
    err = SKGServices::dumpSelectSqliteOrder(getDatabase(), iSqlOrder, oResult, iMode);
    return err;
}

SKGError SKGDocument::executeSelectSqliteOrder(const QString& iSqlOrder, SKGStringListList& oResult) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::executeSelectSqliteOrder", err);
    oResult.clear();
    err = SKGServices::executeSelectSqliteOrder(getDatabase(), iSqlOrder, oResult);
    return err;
}

SKGError SKGDocument::getAttributesDescription(const QString& iTable, SKGServices::SKGAttributesList& oResult) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::getAttributesDescription", err);
    //initialisation
    oResult.clear();

    //Search
    SKGStringListList temporaryResult;
    err = this->executeSelectSqliteOrder("PRAGMA table_info( " % iTable % " );", temporaryResult);
    if (!err) {
        int nblines = temporaryResult.count();
        QString realTable = SKGServices::getRealTable(iTable);

        for (int i = 1; i < nblines; ++i) { // the first one is ignored because it is the headers
            QStringList line = temporaryResult[i];

            SKGServices::SKGAttributeInfo attribute;
            attribute.name = line[1];

            QString attname = realTable % '.' % attribute.name;
            attribute.display = getDisplay(attname);
            if (attribute.display == attname) {
                attribute.display = "";
            }
            attribute.icon = getIcon(attname);
            attribute.type = getAttributeType(attribute.name);
            attribute.notnull = (line[3] == "0");
            attribute.defaultvalue = line[4];
            oResult.push_back(attribute);
        }
    }

    return err;
}

SKGError SKGDocument::getAttributesList(const QString& iTable, QStringList& oResult) const
{
    SKGError err;
    _SKGTRACEINRC(10, "SKGDocument::getAttributesList", err);
    oResult.clear();
    SKGServices::SKGAttributesList attDesc;
    err = getAttributesDescription(iTable, attDesc);
    int nblines = attDesc.count();
    for (int i = 0; !err && i < nblines; ++i) {
        oResult.push_back(attDesc[i].name);
    }
    return err;
}

SKGReport* SKGDocument::getReport()
{
    return new SKGReport(this);
}

#include "skgdocument.moc"

