@Test
  public void shouldClearAllTablesWhenDeletingAllAccounts() {
    Account account = new Account("Test");
    Transaction transaction = new Transaction("Test description");
    Split split = new Split(Money.getZeroInstance(), account.getUID());
    transaction.addSplit(split);
    Account account2 = new Account("Transfer account");
    transaction.addSplit(split.createPair(account2.getUID()));

    mAccountsDbAdapter.addRecord(account);
    mAccountsDbAdapter.addRecord(account2);

    ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
    scheduledAction.setActionUID("Test-uid");
    ScheduledActionDbAdapter scheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance();

    scheduledActionDbAdapter.addRecord(scheduledAction);

    mAccountsDbAdapter.deleteAllRecords();

    assertThat(mAccountsDbAdapter.getRecordsCount()).isZero();
    assertThat(mTransactionsDbAdapter.getRecordsCount()).isZero();
    assertThat(mSplitsDbAdapter.getRecordsCount()).isZero();
    assertThat(scheduledActionDbAdapter.getRecordsCount()).isZero();
  }
  /**
   * Converts all expenses into OFX XML format and adds them to the XML document
   *
   * @param doc DOM document of the OFX expenses.
   * @param parent Parent node for all expenses in report
   */
  private void generateOfx(Document doc, Element parent) {
    Element transactionUid = doc.createElement(OfxHelper.TAG_TRANSACTION_UID);
    // unsolicited because the data exported is not as a result of a request
    transactionUid.appendChild(doc.createTextNode(OfxHelper.UNSOLICITED_TRANSACTION_ID));

    Element statementTransactionResponse =
        doc.createElement(OfxHelper.TAG_STATEMENT_TRANSACTION_RESPONSE);
    statementTransactionResponse.appendChild(transactionUid);

    Element bankmsgs = doc.createElement(OfxHelper.TAG_BANK_MESSAGES_V1);
    bankmsgs.appendChild(statementTransactionResponse);

    parent.appendChild(bankmsgs);

    AccountsDbAdapter accountsDbAdapter = mAccountsDbAdapter;
    for (Account account : mAccountsList) {
      if (account.getTransactionCount() == 0) continue;

      // do not export imbalance accounts for OFX transactions and double-entry disabled
      if (!GnuCashApplication.isDoubleEntryEnabled()
          && account.getName().contains(mContext.getString(R.string.imbalance_account_name)))
        continue;

      // add account details (transactions) to the XML document
      account.toOfx(doc, statementTransactionResponse, mExportParams.getExportStartTime());

      // mark as exported
      accountsDbAdapter.markAsExported(account.getUID());
    }
  }
  @Test
  public void bulkAddAccountsShouldNotModifyTransactions() {
    Account account1 = new Account("AlphaAccount");
    Account account2 = new Account("BetaAccount");
    Transaction transaction = new Transaction("MyTransaction");
    Split split = new Split(Money.getZeroInstance(), account1.getUID());
    transaction.addSplit(split);
    transaction.addSplit(split.createPair(account2.getUID()));
    account1.addTransaction(transaction);
    account2.addTransaction(transaction);

    List<Account> accounts = new ArrayList<>();
    accounts.add(account1);
    accounts.add(account2);

    mAccountsDbAdapter.bulkAddRecords(accounts);

    SplitsDbAdapter splitsDbAdapter = SplitsDbAdapter.getInstance();
    assertThat(
            splitsDbAdapter.getSplitsForTransactionInAccount(
                transaction.getUID(), account1.getUID()))
        .hasSize(1);
    assertThat(
            splitsDbAdapter.getSplitsForTransactionInAccount(
                transaction.getUID(), account2.getUID()))
        .hasSize(1);

    assertThat(mAccountsDbAdapter.getRecord(account1.getUID()).getTransactions()).hasSize(1);
  }
  @Test
  public void shouldRecursivelyDeleteAccount() {
    Account account = new Account("Parent");
    Account account2 = new Account("Child");
    account2.setParentUID(account.getUID());

    Transaction transaction = new Transaction("Random");
    account2.addTransaction(transaction);

    Split split = new Split(Money.getZeroInstance(), account.getUID());
    transaction.addSplit(split);
    transaction.addSplit(split.createPair(account2.getUID()));

    mAccountsDbAdapter.addRecord(account);
    mAccountsDbAdapter.addRecord(account2);

    assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(3);
    assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1);
    assertThat(mSplitsDbAdapter.getRecordsCount()).isEqualTo(2);

    boolean result =
        mAccountsDbAdapter.recursiveDeleteAccount(mAccountsDbAdapter.getID(account.getUID()));
    assertThat(result).isTrue();

    assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(1); // the root account
    assertThat(mTransactionsDbAdapter.getRecordsCount()).isZero();
    assertThat(mSplitsDbAdapter.getRecordsCount()).isZero();
  }
  @Test
  public void shouldAddTransactionsAndSplitsWhenAddingAccounts() {
    Account account = new Account("Test");
    mAccountsDbAdapter.addRecord(account);

    Transaction transaction = new Transaction("Test description");
    Split split = new Split(Money.getZeroInstance(), account.getUID());
    transaction.addSplit(split);
    Account account1 = new Account("Transfer account");
    transaction.addSplit(split.createPair(account1.getUID()));
    account1.addTransaction(transaction);

    mAccountsDbAdapter.addRecord(account1);

    assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1);
    assertThat(mSplitsDbAdapter.getRecordsCount()).isEqualTo(2);
    assertThat(mAccountsDbAdapter.getRecordsCount())
        .isEqualTo(3); // ROOT account automatically added
  }
  @Test
  public void shouldUpdateFullNameAfterParentChange() {
    Account parent = new Account("Test");
    Account child = new Account("Child");

    mAccountsDbAdapter.addRecord(parent);
    mAccountsDbAdapter.addRecord(child);

    child.setParentUID(parent.getUID());
    mAccountsDbAdapter.addRecord(child);

    child = mAccountsDbAdapter.getRecord(child.getUID());
    parent = mAccountsDbAdapter.getRecord(parent.getUID());

    assertThat(mAccountsDbAdapter.getSubAccountCount(parent.getUID())).isEqualTo(1);
    assertThat(parent.getUID()).isEqualTo(child.getParentUID());

    assertThat(child.getFullName()).isEqualTo("Test:Child");
  }
  @Test
  public void deletingTransactionsShouldDeleteSplits() {
    Transaction transaction = new Transaction("");
    Split split = new Split(Money.getZeroInstance(), alphaAccount.getUID());
    transaction.addSplit(split);
    mTransactionsDbAdapter.addRecord(transaction);

    assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(1);

    mTransactionsDbAdapter.deleteRecord(transaction.getUID());
    assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(0);
  }
  @Test
  public void testComputeBalance() {
    Transaction transaction = new Transaction("Compute");
    Money firstSplitAmount = new Money("4.99", DEFAULT_CURRENCY.getCurrencyCode());
    Split split = new Split(firstSplitAmount, alphaAccount.getUID());
    transaction.addSplit(split);
    Money secondSplitAmount = new Money("3.50", DEFAULT_CURRENCY.getCurrencyCode());
    split = new Split(secondSplitAmount, bravoAccount.getUID());
    transaction.addSplit(split);

    mTransactionsDbAdapter.addRecord(transaction);

    // balance is negated because the CASH account has inverse normal balance
    transaction = mTransactionsDbAdapter.getRecord(transaction.getUID());
    Money savedBalance = transaction.getBalance(alphaAccount.getUID());
    assertThat(savedBalance).isEqualTo(firstSplitAmount.negate());

    savedBalance = transaction.getBalance(bravoAccount.getUID());
    assertThat(savedBalance.getNumerator()).isEqualTo(secondSplitAmount.negate().getNumerator());
    assertThat(savedBalance.getCurrency()).isEqualTo(secondSplitAmount.getCurrency());
  }
  @Test
  public void testTransactionsAreTimeSorted() {
    Transaction t1 = new Transaction("T800");
    t1.setTime(System.currentTimeMillis() - 10000);
    Split split = new Split(Money.getZeroInstance(), alphaAccount.getUID());
    t1.addSplit(split);
    t1.addSplit(split.createPair(bravoAccount.getUID()));

    Transaction t2 = new Transaction("T1000");
    t2.setTime(System.currentTimeMillis());
    Split split2 = new Split(new Money("23.50"), bravoAccount.getUID());
    t2.addSplit(split2);
    t2.addSplit(split2.createPair(alphaAccount.getUID()));

    mTransactionsDbAdapter.addRecord(t1);
    mTransactionsDbAdapter.addRecord(t2);

    List<Transaction> transactionsList =
        mTransactionsDbAdapter.getAllTransactionsForAccount(alphaAccount.getUID());
    assertThat(transactionsList).contains(t2, Index.atIndex(0));
    assertThat(transactionsList).contains(t1, Index.atIndex(1));
  }
  @Before
  public void setUp() throws Exception {
    mSplitsDbAdapter = SplitsDbAdapter.getInstance();
    mTransactionsDbAdapter = TransactionsDbAdapter.getInstance();
    mAccountsDbAdapter = AccountsDbAdapter.getInstance();

    alphaAccount = new Account(ALPHA_ACCOUNT_NAME);
    bravoAccount = new Account(BRAVO_ACCOUNT_NAME);

    mAccountsDbAdapter.addRecord(bravoAccount);
    mAccountsDbAdapter.addRecord(alphaAccount);

    mTestSplit = new Split(new Money(BigDecimal.TEN, DEFAULT_CURRENCY), alphaAccount.getUID());
  }
Example #11
0
  /** Tests the foreign key constraint "ON DELETE CASCADE" between accounts and splits */
  @Test
  public void shouldDeleteSplitsWhenAccountDeleted() {
    Account first = new Account(ALPHA_ACCOUNT_NAME);
    first.setUID(ALPHA_ACCOUNT_NAME);
    Account second = new Account(BRAVO_ACCOUNT_NAME);
    second.setUID(BRAVO_ACCOUNT_NAME);

    mAccountsDbAdapter.addRecord(second);
    mAccountsDbAdapter.addRecord(first);

    Transaction transaction = new Transaction("TestTrn");
    Split split = new Split(Money.getZeroInstance(), ALPHA_ACCOUNT_NAME);
    transaction.addSplit(split);
    transaction.addSplit(split.createPair(BRAVO_ACCOUNT_NAME));

    mTransactionsDbAdapter.addRecord(transaction);

    mAccountsDbAdapter.deleteRecord(ALPHA_ACCOUNT_NAME);

    Transaction trxn = mTransactionsDbAdapter.getRecord(transaction.getUID());
    assertThat(trxn.getSplits().size()).isEqualTo(1);
    assertThat(trxn.getSplits().get(0).getAccountUID()).isEqualTo(BRAVO_ACCOUNT_NAME);
  }
Example #12
0
  @Test
  public void shouldComputeAccountBalanceCorrectly() {
    Account account = new Account("Test", Commodity.USD);
    account.setAccountType(AccountType.ASSET); // debit normal account balance
    Account transferAcct = new Account("Transfer");

    mAccountsDbAdapter.addRecord(account);
    mAccountsDbAdapter.addRecord(transferAcct);

    Transaction transaction = new Transaction("Test description");
    mTransactionsDbAdapter.addRecord(transaction);
    Split split = new Split(new Money(BigDecimal.TEN, Commodity.USD), account.getUID());
    split.setTransactionUID(transaction.getUID());
    split.setType(TransactionType.DEBIT);
    mSplitsDbAdapter.addRecord(split);

    split = new Split(new Money("4.99", "USD"), account.getUID());
    split.setTransactionUID(transaction.getUID());
    split.setType(TransactionType.DEBIT);
    mSplitsDbAdapter.addRecord(split);

    split = new Split(new Money("1.19", "USD"), account.getUID());
    split.setTransactionUID(transaction.getUID());
    split.setType(TransactionType.CREDIT);
    mSplitsDbAdapter.addRecord(split);

    split = new Split(new Money("3.49", "EUR"), account.getUID());
    split.setTransactionUID(transaction.getUID());
    split.setType(TransactionType.DEBIT);
    mSplitsDbAdapter.addRecord(split);

    split = new Split(new Money("8.39", "USD"), transferAcct.getUID());
    split.setTransactionUID(transaction.getUID());
    mSplitsDbAdapter.addRecord(split);

    // balance computation ignores the currency of the split
    Money balance = mAccountsDbAdapter.getAccountBalance(account.getUID());
    Money expectedBalance = new Money("17.29", "USD"); // EUR splits should be ignored

    assertThat(balance).isEqualTo(expectedBalance);
  }
  @Test
  public void shouldBalanceTransactionsOnSave() {
    Transaction transaction = new Transaction("Auto balance");
    Split split =
        new Split(
            new Money(BigDecimal.TEN, Currency.getInstance(Money.DEFAULT_CURRENCY_CODE)),
            alphaAccount.getUID());

    transaction.addSplit(split);

    mTransactionsDbAdapter.addRecord(transaction);

    Transaction trn = mTransactionsDbAdapter.getRecord(transaction.getUID());
    assertThat(trn.getSplits()).hasSize(2);

    String imbalanceAccountUID =
        mAccountsDbAdapter.getImbalanceAccountUID(
            Currency.getInstance(Money.DEFAULT_CURRENCY_CODE));
    assertThat(trn.getSplits()).extracting("mAccountUID").contains(imbalanceAccountUID);
  }
Example #14
0
  @Test
  public void simpleAccountListShouldNotContainTransactions() {
    Account account = new Account("Test");
    Transaction transaction = new Transaction("Test description");
    Split split = new Split(Money.getZeroInstance(), account.getUID());
    transaction.addSplit(split);
    Account account1 = new Account("Transfer");
    transaction.addSplit(split.createPair(account1.getUID()));

    mAccountsDbAdapter.addRecord(account);
    mAccountsDbAdapter.addRecord(account1);

    List<Account> accounts = mAccountsDbAdapter.getSimpleAccountList();
    for (Account testAcct : accounts) {
      assertThat(testAcct.getTransactionCount()).isZero();
    }
  }
  /**
   * Exports template accounts
   *
   * <p>Template accounts are just dummy accounts created for use with template transactions
   *
   * @param xmlSerializer XML serializer
   * @param accountList List of template accounts
   * @throws IOException if could not write XML to output stream
   */
  private void exportTemplateAccounts(XmlSerializer xmlSerializer, Collection<Account> accountList)
      throws IOException {
    for (Account account : accountList) {
      xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCOUNT);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION);
      // account name
      xmlSerializer.startTag(null, GncXmlHelper.TAG_NAME);
      xmlSerializer.text(account.getName());
      xmlSerializer.endTag(null, GncXmlHelper.TAG_NAME);
      // account guid
      xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_ID);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
      xmlSerializer.text(account.getUID());
      xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_ID);
      // account type
      xmlSerializer.startTag(null, GncXmlHelper.TAG_TYPE);
      xmlSerializer.text(account.getAccountType().name());
      xmlSerializer.endTag(null, GncXmlHelper.TAG_TYPE);
      // commodity
      xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCOUNT_COMMODITY);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SPACE);
      xmlSerializer.text("template");
      xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SPACE);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_ID);
      String acctCurrencyCode = "template";
      xmlSerializer.text(acctCurrencyCode);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_ID);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCOUNT_COMMODITY);
      // commodity scu
      xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SCU);
      xmlSerializer.text("1");
      xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SCU);

      if (account.getAccountType() != AccountType.ROOT && mRootTemplateAccount != null) {
        xmlSerializer.startTag(null, GncXmlHelper.TAG_PARENT_UID);
        xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
        xmlSerializer.text(mRootTemplateAccount.getUID());
        xmlSerializer.endTag(null, GncXmlHelper.TAG_PARENT_UID);
      }
      xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCOUNT);
    }
  }
Example #16
0
  @Test
  public void shouldAddAccountsToDatabase() {
    Account account1 = new Account("AlphaAccount");
    Account account2 = new Account("BetaAccount");
    Transaction transaction = new Transaction("MyTransaction");
    Split split = new Split(Money.getZeroInstance(), account1.getUID());
    transaction.addSplit(split);
    transaction.addSplit(split.createPair(account2.getUID()));
    account1.addTransaction(transaction);
    account2.addTransaction(transaction);

    mAccountsDbAdapter.addRecord(account1);
    mAccountsDbAdapter.addRecord(account2);

    Account firstAccount = mAccountsDbAdapter.getRecord(account1.getUID());
    assertThat(firstAccount).isNotNull();
    assertThat(firstAccount.getUID()).isEqualTo(account1.getUID());
    assertThat(firstAccount.getFullName()).isEqualTo(account1.getFullName());

    Account secondAccount = mAccountsDbAdapter.getRecord(account2.getUID());
    assertThat(secondAccount).isNotNull();
    assertThat(secondAccount.getUID()).isEqualTo(account2.getUID());

    assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1);
  }
  /**
   * Serializes transactions from the database to XML
   *
   * @param xmlSerializer XML serializer
   * @param exportTemplates Flag whether to export templates or normal transactions
   * @throws IOException if the XML serializer cannot be written to
   */
  private void exportTransactions(XmlSerializer xmlSerializer, boolean exportTemplates)
      throws IOException {
    String where = TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + "=0";
    if (exportTemplates) {
      where = TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + "=1";
    }
    Cursor cursor =
        mTransactionsDbAdapter.fetchTransactionsWithSplits(
            new String[] {
              TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " AS trans_uid",
              TransactionEntry.TABLE_NAME
                  + "."
                  + TransactionEntry.COLUMN_DESCRIPTION
                  + " AS trans_desc",
              TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_NOTES + " AS trans_notes",
              TransactionEntry.TABLE_NAME
                  + "."
                  + TransactionEntry.COLUMN_TIMESTAMP
                  + " AS trans_time",
              TransactionEntry.TABLE_NAME
                  + "."
                  + TransactionEntry.COLUMN_EXPORTED
                  + " AS trans_exported",
              TransactionEntry.TABLE_NAME
                  + "."
                  + TransactionEntry.COLUMN_CURRENCY
                  + " AS trans_currency",
              TransactionEntry.TABLE_NAME
                  + "."
                  + TransactionEntry.COLUMN_CREATED_AT
                  + " AS trans_date_posted",
              TransactionEntry.TABLE_NAME
                  + "."
                  + TransactionEntry.COLUMN_SCHEDX_ACTION_UID
                  + " AS trans_from_sched_action",
              SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_UID + " AS split_uid",
              SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_MEMO + " AS split_memo",
              SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TYPE + " AS split_type",
              SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_VALUE_NUM + " AS split_value_num",
              SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_VALUE_DENOM + " AS split_value_denom",
              SplitEntry.TABLE_NAME
                  + "."
                  + SplitEntry.COLUMN_QUANTITY_NUM
                  + " AS split_quantity_num",
              SplitEntry.TABLE_NAME
                  + "."
                  + SplitEntry.COLUMN_QUANTITY_DENOM
                  + " AS split_quantity_denom",
              SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID + " AS split_acct_uid"
            },
            where,
            null,
            TransactionEntry.TABLE_NAME
                + "."
                + TransactionEntry.COLUMN_TIMESTAMP
                + " ASC , "
                + TransactionEntry.TABLE_NAME
                + "."
                + TransactionEntry.COLUMN_UID
                + " ASC ");
    String lastTrxUID = "";
    Commodity trnCommodity = null;
    String denomString = "100";

    if (exportTemplates) {
      mRootTemplateAccount = new Account("Template Root");
      mRootTemplateAccount.setAccountType(AccountType.ROOT);
      mTransactionToTemplateAccountMap.put(" ", mRootTemplateAccount);
      while (cursor.moveToNext()) {
        Account account = new Account(BaseModel.generateUID());
        account.setAccountType(AccountType.BANK);
        String trnUID = cursor.getString(cursor.getColumnIndexOrThrow("trans_uid"));
        mTransactionToTemplateAccountMap.put(trnUID, account);
      }

      exportTemplateAccounts(xmlSerializer, mTransactionToTemplateAccountMap.values());
      // push cursor back to before the beginning
      cursor.moveToFirst();
      cursor.moveToPrevious();
    }

    while (cursor.moveToNext()) {
      String curTrxUID = cursor.getString(cursor.getColumnIndexOrThrow("trans_uid"));
      if (!lastTrxUID.equals(curTrxUID)) { // new transaction starts
        if (!lastTrxUID.equals("")) { // there's an old transaction, close it
          xmlSerializer.endTag(null, GncXmlHelper.TAG_TRN_SPLITS);
          xmlSerializer.endTag(null, GncXmlHelper.TAG_TRANSACTION);
        }
        // new transaction
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TRANSACTION);
        xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION);
        // transaction id
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TRX_ID);
        xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
        xmlSerializer.text(curTrxUID);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_TRX_ID);
        // currency
        String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow("trans_currency"));
        trnCommodity =
            CommoditiesDbAdapter.getInstance()
                .getCommodity(currencyCode); // Currency.getInstance(currencyCode);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TRX_CURRENCY);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SPACE);
        xmlSerializer.text("ISO4217");
        xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SPACE);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_ID);
        xmlSerializer.text(currencyCode);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_ID);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_TRX_CURRENCY);
        // date posted, time which user put on the transaction
        String strDate =
            GncXmlHelper.formatDate(cursor.getLong(cursor.getColumnIndexOrThrow("trans_time")));
        xmlSerializer.startTag(null, GncXmlHelper.TAG_DATE_POSTED);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TS_DATE);
        xmlSerializer.text(strDate);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_TS_DATE);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_DATE_POSTED);

        // date entered, time when the transaction was actually created
        Timestamp timeEntered =
            Timestamp.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("trans_date_posted")));
        String dateEntered = GncXmlHelper.formatDate(timeEntered.getTime());
        xmlSerializer.startTag(null, GncXmlHelper.TAG_DATE_ENTERED);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TS_DATE);
        xmlSerializer.text(dateEntered);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_TS_DATE);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_DATE_ENTERED);

        // description
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TRN_DESCRIPTION);
        xmlSerializer.text(cursor.getString(cursor.getColumnIndexOrThrow("trans_desc")));
        xmlSerializer.endTag(null, GncXmlHelper.TAG_TRN_DESCRIPTION);
        lastTrxUID = curTrxUID;
        // slots
        ArrayList<String> slotKey = new ArrayList<>();
        ArrayList<String> slotType = new ArrayList<>();
        ArrayList<String> slotValue = new ArrayList<>();

        String notes = cursor.getString(cursor.getColumnIndexOrThrow("trans_notes"));
        boolean exported = cursor.getInt(cursor.getColumnIndexOrThrow("trans_exported")) == 1;
        if (notes != null && notes.length() > 0) {
          slotKey.add(GncXmlHelper.KEY_NOTES);
          slotType.add(GncXmlHelper.ATTR_VALUE_STRING);
          slotValue.add(notes);
        }
        if (!exported) {
          slotKey.add(GncXmlHelper.KEY_EXPORTED);
          slotType.add(GncXmlHelper.ATTR_VALUE_STRING);
          slotValue.add("false");
        }

        String scheduledActionUID =
            cursor.getString(cursor.getColumnIndexOrThrow("trans_from_sched_action"));
        if (scheduledActionUID != null && !scheduledActionUID.isEmpty()) {
          slotKey.add(GncXmlHelper.KEY_FROM_SCHED_ACTION);
          slotType.add(GncXmlHelper.ATTR_VALUE_GUID);
          slotValue.add(scheduledActionUID);
        }
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TRN_SLOTS);
        exportSlots(xmlSerializer, slotKey, slotType, slotValue);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_TRN_SLOTS);

        // splits start
        xmlSerializer.startTag(null, GncXmlHelper.TAG_TRN_SPLITS);
      }
      xmlSerializer.startTag(null, GncXmlHelper.TAG_TRN_SPLIT);
      // split id
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_ID);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
      xmlSerializer.text(cursor.getString(cursor.getColumnIndexOrThrow("split_uid")));
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_ID);
      // memo
      String memo = cursor.getString(cursor.getColumnIndexOrThrow("split_memo"));
      if (memo != null && memo.length() > 0) {
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_MEMO);
        xmlSerializer.text(memo);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_MEMO);
      }
      // reconciled
      xmlSerializer.startTag(null, GncXmlHelper.TAG_RECONCILED_STATE);
      xmlSerializer.text("n");
      xmlSerializer.endTag(null, GncXmlHelper.TAG_RECONCILED_STATE);
      // value, in the transaction's currency
      String trxType = cursor.getString(cursor.getColumnIndexOrThrow("split_type"));
      int splitValueNum = cursor.getInt(cursor.getColumnIndexOrThrow("split_value_num"));
      int splitValueDenom = cursor.getInt(cursor.getColumnIndexOrThrow("split_value_denom"));
      BigDecimal splitAmount = Money.getBigDecimal(splitValueNum, splitValueDenom);
      String strValue = "0/" + denomString;
      if (!exportTemplates) { // when doing normal transaction export
        strValue = (trxType.equals("CREDIT") ? "-" : "") + splitValueNum + "/" + splitValueDenom;
      }
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_VALUE);
      xmlSerializer.text(strValue);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_VALUE);
      // quantity, in the split account's currency
      String splitQuantityNum =
          cursor.getString(cursor.getColumnIndexOrThrow("split_quantity_num"));
      String splitQuantityDenom =
          cursor.getString(cursor.getColumnIndexOrThrow("split_quantity_denom"));
      if (!exportTemplates) {
        strValue =
            (trxType.equals("CREDIT") ? "-" : "") + splitQuantityNum + "/" + splitQuantityDenom;
      }
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_QUANTITY);
      xmlSerializer.text(strValue);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_QUANTITY);
      // account guid
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_ACCOUNT);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
      String splitAccountUID = null;
      if (exportTemplates) {
        // get the UID of the template account
        splitAccountUID = mTransactionToTemplateAccountMap.get(curTrxUID).getUID();
      } else {
        splitAccountUID = cursor.getString(cursor.getColumnIndexOrThrow("split_acct_uid"));
      }
      xmlSerializer.text(splitAccountUID);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_ACCOUNT);

      // if we are exporting a template transaction, then we need to add some extra slots
      if (exportTemplates) {
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_SLOTS);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SLOT);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SLOT_KEY);
        xmlSerializer.text(
            GncXmlHelper.KEY_SCHEDX_ACTION); // FIXME: not all templates may be scheduled actions
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SLOT_KEY);
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SLOT_VALUE);
        xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, "frame");

        List<String> slotKeys = new ArrayList<>();
        List<String> slotTypes = new ArrayList<>();
        List<String> slotValues = new ArrayList<>();
        slotKeys.add(GncXmlHelper.KEY_SPLIT_ACCOUNT_SLOT);
        slotTypes.add(GncXmlHelper.ATTR_VALUE_GUID);
        slotValues.add(cursor.getString(cursor.getColumnIndexOrThrow("split_acct_uid")));
        TransactionType type = TransactionType.valueOf(trxType);
        if (type == TransactionType.CREDIT) {
          slotKeys.add(GncXmlHelper.KEY_CREDIT_FORMULA);
          slotTypes.add(GncXmlHelper.ATTR_VALUE_STRING);
          slotValues.add(GncXmlHelper.formatTemplateSplitAmount(splitAmount));
          slotKeys.add(GncXmlHelper.KEY_CREDIT_NUMERIC);
          slotTypes.add(GncXmlHelper.ATTR_VALUE_NUMERIC);
          slotValues.add(GncXmlHelper.formatSplitAmount(splitAmount, trnCommodity));
        } else {
          slotKeys.add(GncXmlHelper.KEY_DEBIT_FORMULA);
          slotTypes.add(GncXmlHelper.ATTR_VALUE_STRING);
          slotValues.add(GncXmlHelper.formatTemplateSplitAmount(splitAmount));
          slotKeys.add(GncXmlHelper.KEY_DEBIT_NUMERIC);
          slotTypes.add(GncXmlHelper.ATTR_VALUE_NUMERIC);
          slotValues.add(GncXmlHelper.formatSplitAmount(splitAmount, trnCommodity));
        }

        exportSlots(xmlSerializer, slotKeys, slotTypes, slotValues);

        xmlSerializer.endTag(null, GncXmlHelper.TAG_SLOT_VALUE);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SLOT);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_SLOTS);
      }

      xmlSerializer.endTag(null, GncXmlHelper.TAG_TRN_SPLIT);
    }
    if (!lastTrxUID.equals("")) { // there's an unfinished transaction, close it
      xmlSerializer.endTag(null, GncXmlHelper.TAG_TRN_SPLITS);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_TRANSACTION);
    }
    cursor.close();
  }
  /**
   * Serializes {@link ScheduledAction}s from the database to XML
   *
   * @param xmlSerializer XML serializer
   * @throws IOException
   */
  private void exportScheduledTransactions(XmlSerializer xmlSerializer) throws IOException {
    // for now we will export only scheduled transactions to XML
    Cursor cursor =
        mScheduledActionDbAdapter.fetchAllRecords(
            ScheduledActionEntry.COLUMN_TYPE + "=?",
            new String[] {ScheduledAction.ActionType.TRANSACTION.name()});

    while (cursor.moveToNext()) {
      String actionUID =
          cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ACTION_UID));
      Account accountUID = mTransactionToTemplateAccountMap.get(actionUID);

      if (accountUID
          == null) // if the action UID does not belong to a transaction we've seen before, skip it
      continue;

      xmlSerializer.startTag(null, GncXmlHelper.TAG_SCHEDULED_ACTION);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_ID);

      String nameUID = accountUID.getName();
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
      xmlSerializer.text(nameUID);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ID);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_NAME);

      ScheduledAction.ActionType actionType =
          ScheduledAction.ActionType.valueOf(
              cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_TYPE)));
      if (actionType == ScheduledAction.ActionType.TRANSACTION) {
        String description =
            TransactionsDbAdapter.getInstance()
                .getAttribute(actionUID, TransactionEntry.COLUMN_DESCRIPTION);
        xmlSerializer.text(description);
      } else {
        xmlSerializer.text(actionType.name());
      }
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_NAME);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_ENABLED);
      boolean enabled =
          cursor.getShort(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ENABLED)) > 0;
      xmlSerializer.text(enabled ? "y" : "n");
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ENABLED);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE);
      xmlSerializer.text("n"); // we do not want transactions auto-created on the desktop.
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE_NOTIFY);
      xmlSerializer.text(
          "n"); // TODO: if we ever support notifying before creating a scheduled transaction, then
                // update this
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_AUTO_CREATE_NOTIFY);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_ADVANCE_CREATE_DAYS);
      xmlSerializer.text("0");
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ADVANCE_CREATE_DAYS);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_ADVANCE_REMIND_DAYS);
      xmlSerializer.text("0");
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_ADVANCE_REMIND_DAYS);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_INSTANCE_COUNT);
      String scheduledActionUID =
          cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_UID));
      long instanceCount = mScheduledActionDbAdapter.getActionInstanceCount(scheduledActionUID);
      xmlSerializer.text(Long.toString(instanceCount));
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_INSTANCE_COUNT);

      // start date
      String createdTimestamp =
          cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_CREATED_AT));
      long scheduleStartTime = Timestamp.valueOf(createdTimestamp).getTime();
      serializeDate(xmlSerializer, GncXmlHelper.TAG_SX_START, scheduleStartTime);

      long lastRunTime =
          cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_LAST_RUN));
      if (lastRunTime > 0) {
        serializeDate(xmlSerializer, GncXmlHelper.TAG_SX_LAST, lastRunTime);
      }

      long endTime =
          cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_END_TIME));
      if (endTime > 0) {
        // end date
        serializeDate(xmlSerializer, GncXmlHelper.TAG_SX_END, endTime);
      } else { // add number of occurrences
        int totalFrequency =
            cursor.getInt(
                cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY));
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_NUM_OCCUR);
        xmlSerializer.text(Integer.toString(totalFrequency));
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_NUM_OCCUR);

        // remaining occurrences
        int executionCount =
            cursor.getInt(
                cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_EXECUTION_COUNT));
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_REM_OCCUR);
        xmlSerializer.text(Integer.toString(totalFrequency - executionCount));
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_REM_OCCUR);
      }

      String tag = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_TAG));
      if (tag != null && !tag.isEmpty()) {
        xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_TAG);
        xmlSerializer.text(tag);
        xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_TAG);
      }

      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_TEMPL_ACCOUNT);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID);
      xmlSerializer.text(accountUID.getUID());
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_TEMPL_ACCOUNT);

      xmlSerializer.startTag(null, GncXmlHelper.TAG_SX_SCHEDULE);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_RECURRENCE);
      xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.RECURRENCE_VERSION);
      long period =
          cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_PERIOD));
      PeriodType periodType = ScheduledAction.getPeriodType(period);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_RX_MULT);
      xmlSerializer.text(String.valueOf(periodType.getMultiplier()));
      xmlSerializer.endTag(null, GncXmlHelper.TAG_RX_MULT);
      xmlSerializer.startTag(null, GncXmlHelper.TAG_RX_PERIOD_TYPE);
      xmlSerializer.text(periodType.name().toLowerCase());
      xmlSerializer.endTag(null, GncXmlHelper.TAG_RX_PERIOD_TYPE);

      long recurrenceStartTime =
          cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_START_TIME));
      serializeDate(xmlSerializer, GncXmlHelper.TAG_RX_START, recurrenceStartTime);

      xmlSerializer.endTag(null, GncXmlHelper.TAG_RECURRENCE);
      xmlSerializer.endTag(null, GncXmlHelper.TAG_SX_SCHEDULE);

      xmlSerializer.endTag(null, GncXmlHelper.TAG_SCHEDULED_ACTION);
    }
  }
  @Override
  public void endDocument() throws SAXException {
    super.endDocument();
    HashMap<String, String> mapFullName = new HashMap<>(mAccountList.size());
    HashMap<String, Account> mapImbalanceAccount = new HashMap<>();

    // The XML has no ROOT, create one
    if (mRootAccount == null) {
      mRootAccount = new Account("ROOT");
      mRootAccount.setAccountType(AccountType.ROOT);
      mAccountList.add(mRootAccount);
      mAccountMap.put(mRootAccount.getUID(), mRootAccount);
    }

    String imbalancePrefix = AccountsDbAdapter.getImbalanceAccountPrefix();

    // Add all account without a parent to ROOT, and collect top level imbalance accounts
    for (Account account : mAccountList) {
      mapFullName.put(account.getUID(), null);
      boolean topLevel = false;
      if (account.getParentUID() == null && account.getAccountType() != AccountType.ROOT) {
        account.setParentUID(mRootAccount.getUID());
        topLevel = true;
      }
      if (topLevel || (mRootAccount.getUID().equals(account.getParentUID()))) {
        if (account.getName().startsWith(imbalancePrefix)) {
          mapImbalanceAccount.put(account.getName().substring(imbalancePrefix.length()), account);
        }
      }
    }

    // Set the account for created balancing splits to correct imbalance accounts
    for (Split split : mAutoBalanceSplits) {
      String currencyCode = split.getAccountUID();
      Account imbAccount = mapImbalanceAccount.get(currencyCode);
      if (imbAccount == null) {
        imbAccount =
            new Account(imbalancePrefix + currencyCode, Currency.getInstance(currencyCode));
        imbAccount.setParentUID(mRootAccount.getUID());
        imbAccount.setAccountType(AccountType.BANK);
        mapImbalanceAccount.put(currencyCode, imbAccount);
        mAccountList.add(imbAccount);
      }
      split.setAccountUID(imbAccount.getUID());
    }

    java.util.Stack<Account> stack = new Stack<>();
    for (Account account : mAccountList) {
      if (mapFullName.get(account.getUID()) != null) {
        continue;
      }
      stack.push(account);
      String parentAccountFullName;
      while (!stack.isEmpty()) {
        Account acc = stack.peek();
        if (acc.getAccountType() == AccountType.ROOT) {
          // ROOT_ACCOUNT_FULL_NAME should ensure ROOT always sorts first
          mapFullName.put(acc.getUID(), AccountsDbAdapter.ROOT_ACCOUNT_FULL_NAME);
          stack.pop();
          continue;
        }
        String parentUID = acc.getParentUID();
        Account parentAccount = mAccountMap.get(parentUID);
        // ROOT account will be added if not exist, so now anly ROOT
        // has an empty parent
        if (parentAccount.getAccountType() == AccountType.ROOT) {
          // top level account, full name is the same as its name
          mapFullName.put(acc.getUID(), acc.getName());
          stack.pop();
          continue;
        }
        parentAccountFullName = mapFullName.get(parentUID);
        if (parentAccountFullName == null) {
          // non-top-level account, parent full name still unknown
          stack.push(parentAccount);
          continue;
        }
        mapFullName.put(
            acc.getUID(),
            parentAccountFullName + AccountsDbAdapter.ACCOUNT_NAME_SEPARATOR + acc.getName());
        stack.pop();
      }
    }
    for (Account account : mAccountList) {
      account.setFullName(mapFullName.get(account.getUID()));
    }
    long startTime = System.nanoTime();
    mAccountsDbAdapter.beginTransaction();
    Log.d(getClass().getSimpleName(), "bulk insert starts");
    try {
      Log.d(getClass().getSimpleName(), "before clean up db");
      mAccountsDbAdapter.deleteAllRecords();
      Log.d(
          getClass().getSimpleName(),
          String.format("deb clean up done %d ns", System.nanoTime() - startTime));
      long nAccounts = mAccountsDbAdapter.bulkAddRecords(mAccountList);
      Log.d("Handler:", String.format("%d accounts inserted", nAccounts));
      // We need to add scheduled actions first because there is a foreign key constraint on
      // transactions
      // which are generated from scheduled actions (we do auto-create some transactions during
      // import)
      long nSchedActions = mScheduledActionsDbAdapter.bulkAddRecords(mScheduledActionsList);
      Log.d("Handler:", String.format("%d scheduled actions inserted", nSchedActions));

      long nTempTransactions = mTransactionsDbAdapter.bulkAddRecords(mTemplateTransactions);
      Log.d("Handler:", String.format("%d template transactions inserted", nTempTransactions));

      long nTransactions = mTransactionsDbAdapter.bulkAddRecords(mTransactionList);
      Log.d("Handler:", String.format("%d transactions inserted", nTransactions));

      long nPrices = mPricesDbAdapter.bulkAddRecords(mPriceList);
      Log.d(getClass().getSimpleName(), String.format("%d prices inserted", nPrices));

      long endTime = System.nanoTime();
      Log.d(getClass().getSimpleName(), String.format("bulk insert time: %d", endTime - startTime));

      mAccountsDbAdapter.setTransactionSuccessful();
    } finally {
      mAccountsDbAdapter.endTransaction();
    }
  }
  @Override
  public void endElement(String uri, String localName, String qualifiedName) throws SAXException {
    String characterString = mContent.toString().trim();

    if (mIgnoreElement != null) {
      // Ignore everything inside
      if (qualifiedName.equals(mIgnoreElement)) {
        mIgnoreElement = null;
      }
      mContent.setLength(0);
      return;
    }

    switch (qualifiedName) {
      case GncXmlHelper.TAG_NAME:
        mAccount.setName(characterString);
        mAccount.setFullName(characterString);
        break;
      case GncXmlHelper.TAG_ACCT_ID:
        mAccount.setUID(characterString);
        break;
      case GncXmlHelper.TAG_TYPE:
        AccountType accountType = AccountType.valueOf(characterString);
        mAccount.setAccountType(accountType);
        mAccount.setHidden(accountType == AccountType.ROOT); // flag root account as hidden
        break;
      case GncXmlHelper.TAG_COMMODITY_SPACE:
        if (characterString.equals("ISO4217")) {
          mISO4217Currency = true;
        } else {
          // price of non-ISO4217 commodities cannot be handled
          mPrice = null;
        }
        break;
      case GncXmlHelper.TAG_COMMODITY_ID:
        String currencyCode = mISO4217Currency ? characterString : NO_CURRENCY_CODE;
        if (mAccount != null) {
          mAccount.setCurrency(Currency.getInstance(currencyCode));
        }
        if (mTransaction != null) {
          mTransaction.setCurrencyCode(currencyCode);
        }
        if (mPrice != null) {
          if (mPriceCommodity) {
            mPrice.setCommodityUID(mCommoditiesDbAdapter.getCommodityUID(currencyCode));
            mPriceCommodity = false;
          }
          if (mPriceCurrency) {
            mPrice.setCurrencyUID(mCommoditiesDbAdapter.getCommodityUID(currencyCode));
            mPriceCurrency = false;
          }
        }
        break;
      case GncXmlHelper.TAG_ACCT_DESCRIPTION:
        mAccount.setDescription(characterString);
        break;
      case GncXmlHelper.TAG_PARENT_UID:
        mAccount.setParentUID(characterString);
        break;
      case GncXmlHelper.TAG_ACCOUNT:
        if (!mInTemplates) { // we ignore template accounts, we have no use for them
          mAccountList.add(mAccount);
          mAccountMap.put(mAccount.getUID(), mAccount);
          // check ROOT account
          if (mAccount.getAccountType() == AccountType.ROOT) {
            if (mRootAccount == null) {
              mRootAccount = mAccount;
            } else {
              throw new SAXException("Multiple ROOT accounts exist in book");
            }
          }
          // prepare for next input
          mAccount = null;
          // reset ISO 4217 flag for next account
          mISO4217Currency = false;
        }
        break;
      case GncXmlHelper.TAG_SLOT_KEY:
        switch (characterString) {
          case GncXmlHelper.KEY_PLACEHOLDER:
            mInPlaceHolderSlot = true;
            break;
          case GncXmlHelper.KEY_COLOR:
            mInColorSlot = true;
            break;
          case GncXmlHelper.KEY_FAVORITE:
            mInFavoriteSlot = true;
            break;
          case GncXmlHelper.KEY_NOTES:
            mIsNote = true;
            break;
          case GncXmlHelper.KEY_DEFAULT_TRANSFER_ACCOUNT:
            mInDefaultTransferAccount = true;
            break;
          case GncXmlHelper.KEY_EXPORTED:
            mInExported = true;
            break;
          case GncXmlHelper.KEY_SPLIT_ACCOUNT_SLOT:
            mInSplitAccountSlot = true;
            break;
          case GncXmlHelper.KEY_CREDIT_NUMERIC:
            mInCreditNumericSlot = true;
            break;
          case GncXmlHelper.KEY_DEBIT_NUMERIC:
            mInDebitNumericSlot = true;
            break;
        }
        break;
      case GncXmlHelper.TAG_SLOT_VALUE:
        if (mInPlaceHolderSlot) {
          // Log.v(LOG_TAG, "Setting account placeholder flag");
          mAccount.setPlaceHolderFlag(Boolean.parseBoolean(characterString));
          mInPlaceHolderSlot = false;
        } else if (mInColorSlot) {
          // Log.d(LOG_TAG, "Parsing color code: " + characterString);
          String color = characterString.trim();
          // Gnucash exports the account color in format #rrrgggbbb, but we need only #rrggbb.
          // so we trim the last digit in each block, doesn't affect the color much
          if (!color.equals("Not Set")) {
            // avoid known exception, printStackTrace is very time consuming
            if (!Pattern.matches(Account.COLOR_HEX_REGEX, color))
              color = "#" + color.replaceAll(".(.)?", "$1").replace("null", "");
            try {
              if (mAccount != null) mAccount.setColorCode(color);
            } catch (IllegalArgumentException ex) {
              // sometimes the color entry in the account file is "Not set" instead of just blank.
              // So catch!
              Log.e(
                  LOG_TAG, "Invalid color code '" + color + "' for account " + mAccount.getName());
              Crashlytics.logException(ex);
            }
          }
          mInColorSlot = false;
        } else if (mInFavoriteSlot) {
          mAccount.setFavorite(Boolean.parseBoolean(characterString));
          mInFavoriteSlot = false;
        } else if (mIsNote) {
          if (mTransaction != null) {
            mTransaction.setNote(characterString);
            mIsNote = false;
          }
        } else if (mInDefaultTransferAccount) {
          mAccount.setDefaultTransferAccountUID(characterString);
          mInDefaultTransferAccount = false;
        } else if (mInExported) {
          if (mTransaction != null) {
            mTransaction.setExported(Boolean.parseBoolean(characterString));
            mInExported = false;
          }
        } else if (mInTemplates && mInSplitAccountSlot) {
          mSplit.setAccountUID(characterString);
          mInSplitAccountSlot = false;
        } else if (mInTemplates && mInCreditNumericSlot) {
          handleEndOfTemplateNumericSlot(characterString, TransactionType.CREDIT);
        } else if (mInTemplates && mInDebitNumericSlot) {
          handleEndOfTemplateNumericSlot(characterString, TransactionType.DEBIT);
        }
        break;
        // ================  PROCESSING OF TRANSACTION TAGS =====================================
      case GncXmlHelper.TAG_TRX_ID:
        mTransaction.setUID(characterString);
        break;
      case GncXmlHelper.TAG_TRN_DESCRIPTION:
        mTransaction.setDescription(characterString);
        break;
      case GncXmlHelper.TAG_TS_DATE:
        try {
          if (mIsDatePosted && mTransaction != null) {
            mTransaction.setTime(GncXmlHelper.parseDate(characterString));
            mIsDatePosted = false;
          }
          if (mIsDateEntered && mTransaction != null) {
            Timestamp timestamp = new Timestamp(GncXmlHelper.parseDate(characterString));
            mTransaction.setCreatedTimestamp(timestamp);
            mIsDateEntered = false;
          }
          if (mPrice != null) {
            mPrice.setDate(new Timestamp(GncXmlHelper.parseDate(characterString)));
          }
        } catch (ParseException e) {
          Crashlytics.logException(e);
          String message = "Unable to parse transaction time - " + characterString;
          Log.e(LOG_TAG, message + "\n" + e.getMessage());
          Crashlytics.log(message);
          throw new SAXException(message, e);
        }
        break;
      case GncXmlHelper.TAG_RECURRENCE_PERIOD: // for parsing of old backup files
        mRecurrencePeriod = Long.parseLong(characterString);
        mTransaction.setTemplate(mRecurrencePeriod > 0);
        break;
      case GncXmlHelper.TAG_SPLIT_ID:
        mSplit.setUID(characterString);
        break;
      case GncXmlHelper.TAG_SPLIT_MEMO:
        mSplit.setMemo(characterString);
        break;
      case GncXmlHelper.TAG_SPLIT_VALUE:
        try {
          // The value and quantity can have different sign for custom currency(stock).
          // Use the sign of value for split, as it would not be custom currency
          String q = characterString;
          if (q.charAt(0) == '-') {
            mNegativeQuantity = true;
            q = q.substring(1);
          } else {
            mNegativeQuantity = false;
          }
          mValue = GncXmlHelper.parseSplitAmount(characterString).abs(); // use sign from quantity
        } catch (ParseException e) {
          String msg = "Error parsing split quantity - " + characterString;
          Crashlytics.log(msg);
          Crashlytics.logException(e);
          throw new SAXException(msg, e);
        }
        break;
      case GncXmlHelper.TAG_SPLIT_QUANTITY:
        // delay the assignment of currency when the split account is seen
        try {
          mQuantity = GncXmlHelper.parseSplitAmount(characterString).abs();
        } catch (ParseException e) {
          String msg = "Error parsing split quantity - " + characterString;
          Crashlytics.log(msg);
          Crashlytics.logException(e);
          throw new SAXException(msg, e);
        }
        break;
      case GncXmlHelper.TAG_SPLIT_ACCOUNT:
        if (!mInTemplates) {
          // this is intentional: GnuCash XML formats split amounts, credits are negative, debits
          // are positive.
          mSplit.setType(mNegativeQuantity ? TransactionType.CREDIT : TransactionType.DEBIT);
          // the split amount uses the account currency
          mSplit.setQuantity(new Money(mQuantity, getCurrencyForAccount(characterString)));
          // the split value uses the transaction currency
          mSplit.setValue(new Money(mValue, mTransaction.getCurrency()));
          mSplit.setAccountUID(characterString);
        } else {
          if (!mIgnoreTemplateTransaction)
            mTemplateAccountToTransactionMap.put(characterString, mTransaction.getUID());
        }
        break;
      case GncXmlHelper.TAG_TRN_SPLIT:
        mTransaction.addSplit(mSplit);
        break;
      case GncXmlHelper.TAG_TRANSACTION:
        mTransaction.setTemplate(mInTemplates);
        Split imbSplit = mTransaction.getAutoBalanceSplit();
        if (imbSplit != null) {
          mAutoBalanceSplits.add(imbSplit);
        }
        if (mInTemplates) {
          if (!mIgnoreTemplateTransaction) mTemplateTransactions.add(mTransaction);
        } else {
          mTransactionList.add(mTransaction);
        }
        if (mRecurrencePeriod > 0) { // if we find an old format recurrence period, parse it
          mTransaction.setTemplate(true);
          ScheduledAction scheduledAction =
              ScheduledAction.parseScheduledAction(mTransaction, mRecurrencePeriod);
          mScheduledActionsList.add(scheduledAction);
        }
        mRecurrencePeriod = 0;
        mIgnoreTemplateTransaction = true;
        mTransaction = null;
        break;
      case GncXmlHelper.TAG_TEMPLATE_TRANSACTIONS:
        mInTemplates = false;
        break;

        // ========================= PROCESSING SCHEDULED ACTIONS ==================================
      case GncXmlHelper.TAG_SX_ID:
        mScheduledAction.setUID(characterString);
        break;
      case GncXmlHelper.TAG_SX_NAME:
        if (characterString.equals(ScheduledAction.ActionType.BACKUP.name()))
          mScheduledAction.setActionType(ScheduledAction.ActionType.BACKUP);
        else mScheduledAction.setActionType(ScheduledAction.ActionType.TRANSACTION);
        break;
      case GncXmlHelper.TAG_SX_ENABLED:
        mScheduledAction.setEnabled(characterString.equals("y"));
        break;
      case GncXmlHelper.TAG_SX_AUTO_CREATE:
        mScheduledAction.setAutoCreate(characterString.equals("y"));
        break;
      case GncXmlHelper.TAG_SX_NUM_OCCUR:
        mScheduledAction.setTotalFrequency(Integer.parseInt(characterString));
        break;
      case GncXmlHelper.TAG_RX_MULT:
        mRecurrenceMultiplier = Integer.parseInt(characterString);
        break;
      case GncXmlHelper.TAG_RX_PERIOD_TYPE:
        try {
          PeriodType periodType = PeriodType.valueOf(characterString.toUpperCase());
          periodType.setMultiplier(mRecurrenceMultiplier);
          if (mScheduledAction
              != null) // there might be recurrence tags for bugdets and other stuff
          mScheduledAction.setPeriod(periodType);
        } catch (IllegalArgumentException ex) { // the period type constant is not supported
          String msg = "Unsupported period constant: " + characterString;
          Log.e(LOG_TAG, msg);
          Crashlytics.logException(ex);
          mIgnoreScheduledAction = true;
        }
        break;
      case GncXmlHelper.TAG_GDATE:
        try {
          long date = GncXmlHelper.DATE_FORMATTER.parse(characterString).getTime();
          if (mIsScheduledStart && mScheduledAction != null) {
            mScheduledAction.setCreatedTimestamp(new Timestamp(date));
            mIsScheduledStart = false;
          }

          if (mIsScheduledEnd && mScheduledAction != null) {
            mScheduledAction.setEndTime(date);
            mIsScheduledEnd = false;
          }

          if (mIsLastRun && mScheduledAction != null) {
            mScheduledAction.setLastRun(date);
            mIsLastRun = false;
          }

          if (mIsRecurrenceStart && mScheduledAction != null) {
            mScheduledAction.setStartTime(date);
            mIsRecurrenceStart = false;
          }
        } catch (ParseException e) {
          String msg = "Error parsing scheduled action date " + characterString;
          Log.e(LOG_TAG, msg + e.getMessage());
          Crashlytics.log(msg);
          Crashlytics.logException(e);
          throw new SAXException(msg, e);
        }
        break;
      case GncXmlHelper.TAG_SX_TEMPL_ACCOUNT:
        if (mScheduledAction.getActionType() == ScheduledAction.ActionType.TRANSACTION) {
          mScheduledAction.setActionUID(mTemplateAccountToTransactionMap.get(characterString));
        } else {
          mScheduledAction.setActionUID(UUID.randomUUID().toString().replaceAll("-", ""));
        }
        break;
      case GncXmlHelper.TAG_SCHEDULED_ACTION:
        if (mScheduledAction.getActionUID() != null && !mIgnoreScheduledAction) {
          mScheduledActionsList.add(mScheduledAction);
          int count = generateMissedScheduledTransactions(mScheduledAction);
          Log.i(LOG_TAG, String.format("Generated %d transactions from scheduled action", count));
        }
        mRecurrenceMultiplier = 1; // reset it, even though it will be parsed from XML each time
        mIgnoreScheduledAction = false;
        break;
        // price table
      case GncXmlHelper.TAG_PRICE_ID:
        mPrice.setUID(characterString);
        break;
      case GncXmlHelper.TAG_PRICE_SOURCE:
        if (mPrice != null) {
          mPrice.setSource(characterString);
        }
        break;
      case GncXmlHelper.TAG_PRICE_VALUE:
        if (mPrice != null) {
          String[] parts = characterString.split("/");
          if (parts.length != 2) {
            String message = "Illegal price - " + characterString;
            Log.e(LOG_TAG, message);
            Crashlytics.log(message);
            throw new SAXException(message);
          } else {
            mPrice.setValueNum(Long.valueOf(parts[0]));
            mPrice.setValueDenom(Long.valueOf(parts[1]));
            Log.d(
                getClass().getName(),
                "price "
                    + characterString
                    + " .. "
                    + mPrice.getValueNum()
                    + "/"
                    + mPrice.getValueDenom());
          }
        }
        break;
      case GncXmlHelper.TAG_PRICE_TYPE:
        if (mPrice != null) {
          mPrice.setType(characterString);
        }
        break;
      case GncXmlHelper.TAG_PRICE:
        if (mPrice != null) {
          mPriceList.add(mPrice);
          mPrice = null;
        }
        break;
    }

    // reset the accumulated characters
    mContent.setLength(0);
  }