@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 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 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(); }
@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); }
@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(); } }
@Test public void testAddingSplitToTransaction() { Split split = new Split(Money.getZeroInstance(), "Test"); assertThat(split.getTransactionUID()).isEmpty(); Transaction transaction = new Transaction("Random"); transaction.addSplit(split); assertThat(transaction.getUID()).isEqualTo(split.getTransactionUID()); }
@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 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)); }
/** 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); }
@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); }
@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); }