/*
* Copyright (c) 2012 - 2015 Ngewi Fet <ngewif@gmail.com>
* Copyright (c) 2014 Yongxin Wang <fefe.wyx@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gnucash.android.db.adapter;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.DatabaseSchema;
import org.gnucash.android.model.Account;
import org.gnucash.android.model.AccountType;
import org.gnucash.android.model.Commodity;
import org.gnucash.android.model.Money;
import org.gnucash.android.model.Split;
import org.gnucash.android.model.Transaction;
import org.gnucash.android.model.TransactionType;
import org.gnucash.android.util.TimestampHelper;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import static org.gnucash.android.db.DatabaseSchema.AccountEntry;
import static org.gnucash.android.db.DatabaseSchema.SplitEntry;
import static org.gnucash.android.db.DatabaseSchema.TransactionEntry;
/**
* Manages persistence of {@link Account}s in the database
* Handles adding, modifying and deleting of account records.
* @author Ngewi Fet <ngewif@gmail.com>
* @author Yongxin Wang <fefe.wyx@gmail.com>
* @author Oleksandr Tyshkovets <olexandr.tyshkovets@gmail.com>
*/
public class AccountsDbAdapter extends DatabaseAdapter<Account> {
/**
* Separator used for account name hierarchies between parent and child accounts
*/
public static final String ACCOUNT_NAME_SEPARATOR = ":";
/**
* ROOT account full name.
* should ensure the ROOT account's full name will always sort before any other
* account's full name.
*/
public static final String ROOT_ACCOUNT_FULL_NAME = " ";
/**
* Transactions database adapter for manipulating transactions associated with accounts
*/
private final TransactionsDbAdapter mTransactionsAdapter;
/**
* Commodities database adapter for commodity manipulation
*/
private final CommoditiesDbAdapter mCommoditiesDbAdapter;
/**
* Overloaded constructor. Creates an adapter for an already open database
* @param db SQliteDatabase instance
*/
public AccountsDbAdapter(SQLiteDatabase db, TransactionsDbAdapter transactionsDbAdapter) {
super(db, AccountEntry.TABLE_NAME, new String[]{
AccountEntry.COLUMN_NAME ,
AccountEntry.COLUMN_DESCRIPTION ,
AccountEntry.COLUMN_TYPE ,
AccountEntry.COLUMN_CURRENCY ,
AccountEntry.COLUMN_COLOR_CODE ,
AccountEntry.COLUMN_FAVORITE ,
AccountEntry.COLUMN_FULL_NAME ,
AccountEntry.COLUMN_PLACEHOLDER ,
AccountEntry.COLUMN_CREATED_AT ,
AccountEntry.COLUMN_HIDDEN ,
AccountEntry.COLUMN_COMMODITY_UID,
AccountEntry.COLUMN_PARENT_ACCOUNT_UID,
AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID
});
mTransactionsAdapter = transactionsDbAdapter;
mCommoditiesDbAdapter = new CommoditiesDbAdapter(db);
}
/**
* Convenience overloaded constructor.
* This is used when an AccountsDbAdapter object is needed quickly. Otherwise, the other
* constructor {@link #AccountsDbAdapter(SQLiteDatabase, TransactionsDbAdapter)}
* should be used whenever possible
* @param db Database to create an adapter for
*/
public AccountsDbAdapter(SQLiteDatabase db){
super(db, AccountEntry.TABLE_NAME, new String[]{
AccountEntry.COLUMN_NAME ,
AccountEntry.COLUMN_DESCRIPTION ,
AccountEntry.COLUMN_TYPE ,
AccountEntry.COLUMN_CURRENCY ,
AccountEntry.COLUMN_COLOR_CODE ,
AccountEntry.COLUMN_FAVORITE ,
AccountEntry.COLUMN_FULL_NAME ,
AccountEntry.COLUMN_PLACEHOLDER ,
AccountEntry.COLUMN_CREATED_AT ,
AccountEntry.COLUMN_HIDDEN ,
AccountEntry.COLUMN_COMMODITY_UID,
AccountEntry.COLUMN_PARENT_ACCOUNT_UID,
AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID
});
mTransactionsAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db));
mCommoditiesDbAdapter = new CommoditiesDbAdapter(db);
}
/**
* Returns an application-wide instance of this database adapter
* @return Instance of Accounts db adapter
*/
public static AccountsDbAdapter getInstance(){
return GnuCashApplication.getAccountsDbAdapter();
}
/**
* Adds an account to the database.
* If an account already exists in the database with the same GUID, it is replaced.
* @param account {@link Account} to be inserted to database
*/
@Override
public void addRecord(@NonNull Account account, UpdateMethod updateMethod){
Log.d(LOG_TAG, "Replace account to db");
//in-case the account already existed, we want to update the templates based on it as well
List<Transaction> templateTransactions = mTransactionsAdapter.getScheduledTransactionsForAccount(account.getUID());
super.addRecord(account, updateMethod);
String accountUID = account.getUID();
//now add transactions if there are any
if (account.getAccountType() != AccountType.ROOT){
//update the fully qualified account name
updateRecord(accountUID, AccountEntry.COLUMN_FULL_NAME, getFullyQualifiedAccountName(accountUID));
for (Transaction t : account.getTransactions()) {
t.setCommodity(account.getCommodity());
mTransactionsAdapter.addRecord(t, updateMethod);
}
for (Transaction transaction : templateTransactions) {
mTransactionsAdapter.addRecord(transaction, UpdateMethod.update);
}
}
}
/**
* Adds some accounts and their transactions to the database in bulk.
* <p>If an account already exists in the database with the same GUID, it is replaced.
* This function will NOT try to determine the full name
* of the accounts inserted, full names should be generated prior to the insert.
* <br>All or none of the accounts will be inserted;</p>
* @param accountList {@link Account} to be inserted to database
* @return number of rows inserted
*/
@Override
public long bulkAddRecords(@NonNull List<Account> accountList, UpdateMethod updateMethod){
//scheduled transactions are not fetched from the database when getting account transactions
//so we retrieve those which affect this account and then re-save them later
//this is necessary because the database has ON DELETE CASCADE between accounts and splits
//and all accounts are editing via SQL REPLACE
//// TODO: 20.04.2016 Investigate if we can safely remove updating the transactions when bulk updating accounts
List<Transaction> transactionList = new ArrayList<>(accountList.size()*2);
for (Account account : accountList) {
transactionList.addAll(account.getTransactions());
transactionList.addAll(mTransactionsAdapter.getScheduledTransactionsForAccount(account.getUID()));
}
long nRow = super.bulkAddRecords(accountList, updateMethod);
if (nRow > 0 && !transactionList.isEmpty()){
mTransactionsAdapter.bulkAddRecords(transactionList, updateMethod);
}
return nRow;
}
@Override
protected @NonNull SQLiteStatement setBindings(@NonNull SQLiteStatement stmt, @NonNull final Account account) {
stmt.clearBindings();
stmt.bindString(1, account.getName());
if (account.getDescription() != null)
stmt.bindString(2, account.getDescription());
stmt.bindString(3, account.getAccountType().name());
stmt.bindString(4, account.getCommodity().getCurrencyCode());
if (account.getColor() != Account.DEFAULT_COLOR) {
stmt.bindString(5, convertToRGBHexString(account.getColor()));
}
stmt.bindLong(6, account.isFavorite() ? 1 : 0);
stmt.bindString(7, account.getFullName());
stmt.bindLong(8, account.isPlaceholderAccount() ? 1 : 0);
stmt.bindString(9, TimestampHelper.getUtcStringFromTimestamp(account.getCreatedTimestamp()));
stmt.bindLong(10, account.isHidden() ? 1 : 0);
stmt.bindString(11, account.getCommodity().getUID());
String parentAccountUID = account.getParentUID();
if (parentAccountUID == null && account.getAccountType() != AccountType.ROOT) {
parentAccountUID = getOrCreateGnuCashRootAccountUID();
}
if (parentAccountUID != null) {
stmt.bindString(12, parentAccountUID);
}
if (account.getDefaultTransferAccountUID() != null) {
stmt.bindString(13, account.getDefaultTransferAccountUID());
}
stmt.bindString(14, account.getUID());
return stmt;
}
private String convertToRGBHexString(int color) {
return String.format("#%06X", (0xFFFFFF & color));
}
/**
* Marks all transactions for a given account as exported
* @param accountUID Unique ID of the record to be marked as exported
* @return Number of records marked as exported
*/
public int markAsExported(String accountUID){
ContentValues contentValues = new ContentValues();
contentValues.put(TransactionEntry.COLUMN_EXPORTED, 1);
return mDb.update(
TransactionEntry.TABLE_NAME,
contentValues,
TransactionEntry.COLUMN_UID + " IN ( " +
"SELECT DISTINCT " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID +
" FROM " + TransactionEntry.TABLE_NAME + " , " + SplitEntry.TABLE_NAME + " ON " +
TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " = " +
SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID + " , " +
AccountEntry.TABLE_NAME + " ON " + SplitEntry.TABLE_NAME + "." +
SplitEntry.COLUMN_ACCOUNT_UID + " = " + AccountEntry.TABLE_NAME + "." +
AccountEntry.COLUMN_UID + " WHERE " + AccountEntry.TABLE_NAME + "." +
AccountEntry.COLUMN_UID + " = ? "
+ " ) ",
new String[]{accountUID}
);
}
/**
* This feature goes through all the rows in the accounts and changes value for <code>columnKey</code> to <code>newValue</code><br/>
* The <code>newValue</code> parameter is taken as string since SQLite typically stores everything as text.
* <p><b>This method affects all rows, exercise caution when using it</b></p>
* @param columnKey Name of column to be updated
* @param newValue New value to be assigned to the columnKey
* @return Number of records affected
*/
public int updateAllAccounts(String columnKey, String newValue){
ContentValues contentValues = new ContentValues();
if (newValue == null) {
contentValues.putNull(columnKey);
} else {
contentValues.put(columnKey, newValue);
}
return mDb.update(AccountEntry.TABLE_NAME, contentValues, null, null);
}
/**
* Updates a specific entry of an account
* @param accountId Database record ID of the account to be updated
* @param columnKey Name of column to be updated
* @param newValue New value to be assigned to the columnKey
* @return Number of records affected
*/
public int updateAccount(long accountId, String columnKey, String newValue){
return updateRecord(AccountEntry.TABLE_NAME, accountId, columnKey, newValue);
}
/**
* This method goes through all the children of {@code accountUID} and updates the parent account
* to {@code newParentAccountUID}. The fully qualified account names for all descendant accounts will also be updated.
* @param accountUID GUID of the account
* @param newParentAccountUID GUID of the new parent account
*/
public void reassignDescendantAccounts(@NonNull String accountUID, @NonNull String newParentAccountUID) {
List<String> descendantAccountUIDs = getDescendantAccountUIDs(accountUID, null, null);
if (descendantAccountUIDs.size() > 0) {
List<Account> descendantAccounts = getSimpleAccountList(
AccountEntry.COLUMN_UID + " IN ('" + TextUtils.join("','", descendantAccountUIDs) + "')",
null,
null
);
HashMap<String, Account> mapAccounts = new HashMap<>();
for (Account account : descendantAccounts)
mapAccounts.put(account.getUID(), account);
String parentAccountFullName;
if (getAccountType(newParentAccountUID) == AccountType.ROOT) {
parentAccountFullName = "";
} else {
parentAccountFullName = getAccountFullName(newParentAccountUID);
}
ContentValues contentValues = new ContentValues();
for (String acctUID : descendantAccountUIDs) {
Account acct = mapAccounts.get(acctUID);
if (accountUID.equals(acct.getParentUID())) {
// direct descendant
acct.setParentUID(newParentAccountUID);
if (parentAccountFullName == null || parentAccountFullName.isEmpty()) {
acct.setFullName(acct.getName());
} else {
acct.setFullName(parentAccountFullName + ACCOUNT_NAME_SEPARATOR + acct.getName());
}
// update DB
contentValues.clear();
contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, newParentAccountUID);
contentValues.put(AccountEntry.COLUMN_FULL_NAME, acct.getFullName());
mDb.update(
AccountEntry.TABLE_NAME, contentValues,
AccountEntry.COLUMN_UID + " = ?",
new String[]{acct.getUID()}
);
} else {
// indirect descendant
acct.setFullName(
mapAccounts.get(acct.getParentUID()).getFullName() +
ACCOUNT_NAME_SEPARATOR + acct.getName()
);
// update DB
contentValues.clear();
contentValues.put(AccountEntry.COLUMN_FULL_NAME, acct.getFullName());
mDb.update(
AccountEntry.TABLE_NAME, contentValues,
AccountEntry.COLUMN_UID + " = ?",
new String[]{acct.getUID()}
);
}
}
}
}
/**
* Deletes an account and its transactions, and all its sub-accounts and their transactions.
* <p>Not only the splits belonging to the account and its descendants will be deleted, rather,
* the complete transactions associated with this account and its descendants
* (i.e. as long as the transaction has at least one split belonging to one of the accounts).
* This prevents an split imbalance from being caused.</p>
* <p>If you want to preserve transactions, make sure to first reassign the children accounts (see {@link #reassignDescendantAccounts(String, String)}
* before calling this method. This method will however not delete a root account. </p>
* <p><b>This method does a thorough delete, use with caution!!!</b></p>
* @param accountId Database record ID of account
* @return <code>true</code> if the account and subaccounts were all successfully deleted, <code>false</code> if
* even one was not deleted
* @see #reassignDescendantAccounts(String, String)
*/
public boolean recursiveDeleteAccount(long accountId){
String accountUID = getUID(accountId);
if (getAccountType(accountUID) == AccountType.ROOT) {
// refuse to delete ROOT
return false;
}
Log.d(LOG_TAG, "Delete account with rowId with its transactions and sub-accounts: " + accountId);
List<String> descendantAccountUIDs = getDescendantAccountUIDs(accountUID, null, null);
mDb.beginTransaction();
try {
descendantAccountUIDs.add(accountUID); //add account to descendants list just for convenience
for (String descendantAccountUID : descendantAccountUIDs) {
mTransactionsAdapter.deleteTransactionsForAccount(descendantAccountUID);
}
String accountUIDList = "'" + TextUtils.join("','", descendantAccountUIDs) + "'";
// delete accounts
long deletedCount = mDb.delete(
AccountEntry.TABLE_NAME,
AccountEntry.COLUMN_UID + " IN (" + accountUIDList + ")",
null
);
//if we delete some accounts, reset the default transfer account to NULL
//there is also a database trigger from db version > 12
if (deletedCount > 0){
ContentValues contentValues = new ContentValues();
contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID);
mDb.update(mTableName, contentValues,
AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " IN (" + accountUIDList + ")",
null);
}
mDb.setTransactionSuccessful();
return true;
}
finally {
mDb.endTransaction();
}
}
/**
* Builds an account instance with the provided cursor and loads its corresponding transactions.
*
* @param c Cursor pointing to account record in database
* @return {@link Account} object constructed from database record
*/
@Override
public Account buildModelInstance(@NonNull final Cursor c){
Account account = buildSimpleAccountInstance(c);
account.setTransactions(mTransactionsAdapter.getAllTransactionsForAccount(account.getUID()));
return account;
}
/**
* Builds an account instance with the provided cursor and loads its corresponding transactions.
* <p>The method will not move the cursor position, so the cursor should already be pointing
* to the account record in the database<br/>
* <b>Note</b> Unlike {@link #buildModelInstance(android.database.Cursor)} this method will not load transactions</p>
*
* @param c Cursor pointing to account record in database
* @return {@link Account} object constructed from database record
*/
private Account buildSimpleAccountInstance(Cursor c) {
Account account = new Account(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_NAME)));
populateBaseModelAttributes(c, account);
String description = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_DESCRIPTION));
account.setDescription(description == null ? "" : description);
account.setParentUID(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_PARENT_ACCOUNT_UID)));
account.setAccountType(AccountType.valueOf(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_TYPE))));
String currencyCode = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY));
account.setCommodity(mCommoditiesDbAdapter.getCommodity(currencyCode));
account.setPlaceHolderFlag(c.getInt(c.getColumnIndexOrThrow(AccountEntry.COLUMN_PLACEHOLDER)) == 1);
account.setDefaultTransferAccountUID(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID)));
String color = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_COLOR_CODE));
if (color != null)
account.setColor(color);
account.setFavorite(c.getInt(c.getColumnIndexOrThrow(AccountEntry.COLUMN_FAVORITE)) == 1);
account.setFullName(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_FULL_NAME)));
account.setHidden(c.getInt(c.getColumnIndexOrThrow(AccountEntry.COLUMN_HIDDEN)) == 1);
return account;
}
/**
* Returns the unique ID of the parent account of the account with unique ID <code>uid</code>
* If the account has no parent, null is returned
* @param uid Unique Identifier of account whose parent is to be returned. Should not be null
* @return DB record UID of the parent account, null if the account has no parent
*/
public String getParentAccountUID(@NonNull String uid){
Cursor cursor = mDb.query(AccountEntry.TABLE_NAME,
new String[]{AccountEntry.COLUMN_PARENT_ACCOUNT_UID},
AccountEntry.COLUMN_UID + " = ?",
new String[]{uid},
null, null, null, null);
try {
if (cursor.moveToFirst()) {
Log.d(LOG_TAG, "Found parent account UID, returning value");
return cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_PARENT_ACCOUNT_UID));
} else {
return null;
}
} finally {
cursor.close();
}
}
/**
* Returns the color code for the account in format #rrggbb
* @param accountId Database row ID of the account
* @return String color code of account or null if none
*/
public String getAccountColorCode(long accountId){
Cursor c = mDb.query(AccountEntry.TABLE_NAME,
new String[]{AccountEntry._ID, AccountEntry.COLUMN_COLOR_CODE},
AccountEntry._ID + "=" + accountId,
null, null, null, null);
try {
if (c.moveToFirst()) {
return c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_COLOR_CODE));
}
else {
return null;
}
} finally {
c.close();
}
}
/**
* Overloaded method. Resolves the account unique ID from the row ID and makes a call to {@link #getAccountType(String)}
* @param accountId Database row ID of the account
* @return {@link AccountType} of the account
*/
public AccountType getAccountType(long accountId){
return getAccountType(getUID(accountId));
}
/**
* Returns a list of all account entries in the system (includes root account)
* No transactions are loaded, just the accounts
* @return List of {@link Account}s in the database
*/
public List<Account> getSimpleAccountList(){
LinkedList<Account> accounts = new LinkedList<>();
Cursor c = fetchAccounts(null, null, AccountEntry.COLUMN_FULL_NAME + " ASC");
try {
while (c.moveToNext()) {
accounts.add(buildSimpleAccountInstance(c));
}
}
finally {
c.close();
}
return accounts;
}
/**
* Returns a list of all account entries in the system (includes root account)
* No transactions are loaded, just the accounts
* @return List of {@link Account}s in the database
*/
public List<Account> getSimpleAccountList(String where, String[] whereArgs, String orderBy){
LinkedList<Account> accounts = new LinkedList<>();
Cursor c = fetchAccounts(where, whereArgs, orderBy);
try {
while (c.moveToNext()) {
accounts.add(buildSimpleAccountInstance(c));
}
}
finally {
c.close();
}
return accounts;
}
/**
* Returns a list of accounts which have transactions that have not been exported yet
* @param lastExportTimeStamp Timestamp after which to any transactions created/modified should be exported
* @return List of {@link Account}s with unexported transactions
*/
public List<Account> getExportableAccounts(Timestamp lastExportTimeStamp){
LinkedList<Account> accountsList = new LinkedList<>();
Cursor cursor = mDb.query(
TransactionEntry.TABLE_NAME + " , " + SplitEntry.TABLE_NAME +
" ON " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " = " +
SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID + " , " +
AccountEntry.TABLE_NAME + " ON " + AccountEntry.TABLE_NAME + "." +
AccountEntry.COLUMN_UID + " = " + SplitEntry.TABLE_NAME + "." +
SplitEntry.COLUMN_ACCOUNT_UID,
new String[]{AccountEntry.TABLE_NAME + ".*"},
TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_MODIFIED_AT + " > ?",
new String[]{TimestampHelper.getUtcStringFromTimestamp(lastExportTimeStamp)},
AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID,
null,
null
);
try {
while (cursor.moveToNext()) {
accountsList.add(buildModelInstance(cursor));
}
}
finally {
cursor.close();
}
return accountsList;
}
/**
* Retrieves the unique ID of the imbalance account for a particular currency (creates the imbalance account
* on demand if necessary)
* @param commodity Commodity for the imbalance account
* @return String unique ID of the account
*/
public String getOrCreateImbalanceAccountUID(Commodity commodity){
String imbalanceAccountName = getImbalanceAccountName(commodity);
String uid = findAccountUidByFullName(imbalanceAccountName);
if (uid == null){
Account account = new Account(imbalanceAccountName, commodity);
account.setAccountType(AccountType.BANK);
account.setParentUID(getOrCreateGnuCashRootAccountUID());
account.setHidden(!GnuCashApplication.isDoubleEntryEnabled());
account.setColor("#964B00");
addRecord(account, UpdateMethod.insert);
uid = account.getUID();
}
return uid;
}
/**
* Returns the GUID of the imbalance account for the commodity
*
* <p>This method will not create the imbalance account if it doesn't exist</p>
*
* @param commodity Commodity for the imbalance account
* @return GUID of the account or null if the account doesn't exist yet
* @see #getOrCreateImbalanceAccountUID(Commodity)
*/
public String getImbalanceAccountUID(Commodity commodity){
String imbalanceAccountName = getImbalanceAccountName(commodity);
return findAccountUidByFullName(imbalanceAccountName);
}
/**
* Creates the account with the specified name and returns its unique identifier.
* <p>If a full hierarchical account name is provided, then the whole hierarchy is created and the
* unique ID of the last account (at bottom) of the hierarchy is returned</p>
* @param fullName Fully qualified name of the account
* @param accountType Type to assign to all accounts created
* @return String unique ID of the account at bottom of hierarchy
*/
public String createAccountHierarchy(String fullName, AccountType accountType) {
if ("".equals(fullName)) {
throw new IllegalArgumentException("fullName cannot be empty");
}
String[] tokens = fullName.trim().split(ACCOUNT_NAME_SEPARATOR);
String uid = getOrCreateGnuCashRootAccountUID();
String parentName = "";
ArrayList<Account> accountsList = new ArrayList<>();
for (String token : tokens) {
parentName += token;
String parentUID = findAccountUidByFullName(parentName);
if (parentUID != null) { //the parent account exists, don't recreate
uid = parentUID;
} else {
Account account = new Account(token);
account.setAccountType(accountType);
account.setParentUID(uid); //set its parent
account.setFullName(parentName);
accountsList.add(account);
uid = account.getUID();
}
parentName += ACCOUNT_NAME_SEPARATOR;
}
if (accountsList.size() > 0) {
bulkAddRecords(accountsList, UpdateMethod.insert);
}
// if fullName is not empty, loop will be entered and then uid will never be null
//noinspection ConstantConditions
return uid;
}
/**
* Returns the unique ID of the opening balance account or creates one if necessary
* @return String unique ID of the opening balance account
*/
public String getOrCreateOpeningBalanceAccountUID() {
String openingBalanceAccountName = getOpeningBalanceAccountFullName();
String uid = findAccountUidByFullName(openingBalanceAccountName);
if (uid == null) {
uid = createAccountHierarchy(openingBalanceAccountName, AccountType.EQUITY);
}
return uid;
}
/**
* Finds an account unique ID by its full name
* @param fullName Fully qualified name of the account
* @return String unique ID of the account or null if no match is found
*/
public String findAccountUidByFullName(String fullName){
Cursor c = mDb.query(AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_UID},
AccountEntry.COLUMN_FULL_NAME + "= ?", new String[]{fullName},
null, null, null, "1");
try {
if (c.moveToNext()) {
return c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_UID));
} else {
return null;
}
} finally {
c.close();
}
}
/**
* Returns a cursor to all account records in the database.
* GnuCash ROOT accounts and hidden accounts will <b>not</b> be included in the result set
* @return {@link Cursor} to all account records
*/
@Override
public Cursor fetchAllRecords(){
Log.v(LOG_TAG, "Fetching all accounts from db");
String selection = AccountEntry.COLUMN_HIDDEN + " = 0 AND " + AccountEntry.COLUMN_TYPE + " != ?" ;
return mDb.query(AccountEntry.TABLE_NAME,
null,
selection,
new String[]{AccountType.ROOT.name()},
null, null,
AccountEntry.COLUMN_NAME + " ASC");
}
/**
* Returns a cursor to all account records in the database ordered by full name.
* GnuCash ROOT accounts and hidden accounts will not be included in the result set.
* @return {@link Cursor} to all account records
*/
public Cursor fetchAllRecordsOrderedByFullName(){
Log.v(LOG_TAG, "Fetching all accounts from db");
String selection = AccountEntry.COLUMN_HIDDEN + " = 0 AND " + AccountEntry.COLUMN_TYPE + " != ?" ;
return mDb.query(AccountEntry.TABLE_NAME,
null,
selection,
new String[]{AccountType.ROOT.name()},
null, null,
AccountEntry.COLUMN_FULL_NAME + " ASC");
}
/**
* Returns a Cursor set of accounts which fulfill <code>where</code>
* and ordered by <code>orderBy</code>
* @param where SQL WHERE statement without the 'WHERE' itself
* @param whereArgs args to where clause
* @param orderBy orderBy clause
* @return Cursor set of accounts which fulfill <code>where</code>
*/
public Cursor fetchAccounts(@Nullable String where, @Nullable String[] whereArgs, @Nullable String orderBy){
if (orderBy == null){
orderBy = AccountEntry.COLUMN_NAME + " ASC";
}
Log.v(LOG_TAG, "Fetching all accounts from db where " + where + " order by " + orderBy);
return mDb.query(AccountEntry.TABLE_NAME,
null, where, whereArgs, null, null,
orderBy);
}
/**
* Returns a Cursor set of accounts which fulfill <code>where</code>
* <p>This method returns the accounts list sorted by the full account name</p>
* @param where SQL WHERE statement without the 'WHERE' itself
* @param whereArgs where args
* @return Cursor set of accounts which fulfill <code>where</code>
*/
public Cursor fetchAccountsOrderedByFullName(String where, String[] whereArgs) {
Log.v(LOG_TAG, "Fetching all accounts from db where " + where);
return mDb.query(AccountEntry.TABLE_NAME,
null, where, whereArgs, null, null,
AccountEntry.COLUMN_FULL_NAME + " ASC");
}
/**
* Returns a Cursor set of accounts which fulfill <code>where</code>
* <p>This method returns the favorite accounts first, sorted by name, and then the other accounts,
* sorted by name.</p>
* @param where SQL WHERE statement without the 'WHERE' itself
* @param whereArgs where args
* @return Cursor set of accounts which fulfill <code>where</code>
*/
public Cursor fetchAccountsOrderedByFavoriteAndFullName(String where, String[] whereArgs) {
Log.v(LOG_TAG, "Fetching all accounts from db where " + where + " order by Favorite then Name");
return mDb.query(AccountEntry.TABLE_NAME,
null, where, whereArgs, null, null,
AccountEntry.COLUMN_FAVORITE + " DESC, " + AccountEntry.COLUMN_FULL_NAME + " ASC");
}
/**
* Returns the balance of an account while taking sub-accounts into consideration
* @return Account Balance of an account including sub-accounts
*/
public Money getAccountBalance(String accountUID){
return computeBalance(accountUID, -1, -1);
}
/**
* Returns the balance of an account within the specified time range while taking sub-accounts into consideration
* @param accountUID the account's UUID
* @param startTimestamp the start timestamp of the time range
* @param endTimestamp the end timestamp of the time range
* @return the balance of an account within the specified range including sub-accounts
*/
public Money getAccountBalance(String accountUID, long startTimestamp, long endTimestamp) {
return computeBalance(accountUID, startTimestamp, endTimestamp);
}
/**
* Compute the account balance for all accounts with the specified type within a specific duration
* @param accountType Account Type for which to compute balance
* @param startTimestamp Begin time for the duration in milliseconds
* @param endTimestamp End time for duration in milliseconds
* @return Account balance
*/
public Money getAccountBalance(AccountType accountType, long startTimestamp, long endTimestamp){
Cursor cursor = fetchAccounts(AccountEntry.COLUMN_TYPE + "= ?",
new String[]{accountType.name()}, null);
List<String> accountUidList = new ArrayList<>();
while (cursor.moveToNext()){
String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID));
accountUidList.add(accountUID);
}
cursor.close();
boolean hasDebitNormalBalance = accountType.hasDebitNormalBalance();
String currencyCode = GnuCashApplication.getDefaultCurrencyCode();
Log.d(LOG_TAG, "all account list : " + accountUidList.size());
SplitsDbAdapter splitsDbAdapter = mTransactionsAdapter.getSplitDbAdapter();
return (startTimestamp == -1 && endTimestamp == -1)
? splitsDbAdapter.computeSplitBalance(accountUidList, currencyCode, hasDebitNormalBalance)
: splitsDbAdapter.computeSplitBalance(accountUidList, currencyCode, hasDebitNormalBalance, startTimestamp, endTimestamp);
}
/**
* Returns the account balance for all accounts types specified
* @param accountTypes List of account types
* @param start Begin timestamp for transactions
* @param end End timestamp of transactions
* @return Money balance of the account types
*/
public Money getAccountBalance(List<AccountType> accountTypes, long start, long end){
Money balance = Money.createZeroInstance(GnuCashApplication.getDefaultCurrencyCode());
for (AccountType accountType : accountTypes) {
balance = balance.add(getAccountBalance(accountType, start, end));
}
return balance;
}
private Money computeBalance(String accountUID, long startTimestamp, long endTimestamp) {
Log.d(LOG_TAG, "Computing account balance for account ID " + accountUID);
String currencyCode = mTransactionsAdapter.getAccountCurrencyCode(accountUID);
boolean hasDebitNormalBalance = getAccountType(accountUID).hasDebitNormalBalance();
List<String> accountsList = getDescendantAccountUIDs(accountUID,
null, null);
accountsList.add(0, accountUID);
Log.d(LOG_TAG, "all account list : " + accountsList.size());
SplitsDbAdapter splitsDbAdapter = mTransactionsAdapter.getSplitDbAdapter();
return (startTimestamp == -1 && endTimestamp == -1)
? splitsDbAdapter.computeSplitBalance(accountsList, currencyCode, hasDebitNormalBalance)
: splitsDbAdapter.computeSplitBalance(accountsList, currencyCode, hasDebitNormalBalance, startTimestamp, endTimestamp);
}
/**
* Returns the balance of account list within the specified time range. The default currency
* takes as base currency.
* @param accountUIDList list of account UIDs
* @param startTimestamp the start timestamp of the time range
* @param endTimestamp the end timestamp of the time range
* @return Money balance of account list
*/
public Money getAccountsBalance(@NonNull List<String> accountUIDList, long startTimestamp, long endTimestamp) {
String currencyCode = GnuCashApplication.getDefaultCurrencyCode();
Money balance = Money.createZeroInstance(currencyCode);
if (accountUIDList.isEmpty())
return balance;
boolean hasDebitNormalBalance = getAccountType(accountUIDList.get(0)).hasDebitNormalBalance();
SplitsDbAdapter splitsDbAdapter = mTransactionsAdapter.getSplitDbAdapter();
Money splitSum = (startTimestamp == -1 && endTimestamp == -1)
? splitsDbAdapter.computeSplitBalance(accountUIDList, currencyCode, hasDebitNormalBalance)
: splitsDbAdapter.computeSplitBalance(accountUIDList, currencyCode, hasDebitNormalBalance, startTimestamp, endTimestamp);
return balance.add(splitSum);
}
/**
* Retrieve all descendant accounts of an account
* Note, in filtering, once an account is filtered out, all its descendants
* will also be filtered out, even they don't meet the filter where
* @param accountUID The account to retrieve descendant accounts
* @param where Condition to filter accounts
* @param whereArgs Condition args to filter accounts
* @return The descendant accounts list.
*/
public List<String> getDescendantAccountUIDs(String accountUID, String where, String[] whereArgs) {
// accountsList will hold accountUID with all descendant accounts.
// accountsListLevel will hold descendant accounts of the same level
ArrayList<String> accountsList = new ArrayList<>();
ArrayList<String> accountsListLevel = new ArrayList<>();
accountsListLevel.add(accountUID);
for (;;) {
Cursor cursor = mDb.query(AccountEntry.TABLE_NAME,
new String[]{AccountEntry.COLUMN_UID},
AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IN ( '" + TextUtils.join("' , '", accountsListLevel) + "' )" +
(where == null ? "" : " AND " + where),
whereArgs, null, null, null);
accountsListLevel.clear();
if (cursor != null) {
try {
int columnIndex = cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID);
while (cursor.moveToNext()) {
accountsListLevel.add(cursor.getString(columnIndex));
}
} finally {
cursor.close();
}
}
if (accountsListLevel.size() > 0) {
accountsList.addAll(accountsListLevel);
}
else {
break;
}
}
return accountsList;
}
/**
* Returns a cursor to the dataset containing sub-accounts of the account with record ID <code>accoundId</code>
* @param accountUID GUID of the parent account
* @return {@link Cursor} to the sub accounts data set
*/
public Cursor fetchSubAccounts(String accountUID) {
Log.v(LOG_TAG, "Fetching sub accounts for account id " + accountUID);
String selection = AccountEntry.COLUMN_HIDDEN + " = 0 AND "
+ AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " = ?";
return mDb.query(AccountEntry.TABLE_NAME,
null,
selection,
new String[]{accountUID}, null, null, AccountEntry.COLUMN_NAME + " ASC");
}
/**
* Returns the top level accounts i.e. accounts with no parent or with the GnuCash ROOT account as parent
* @return Cursor to the top level accounts
*/
public Cursor fetchTopLevelAccounts() {
//condition which selects accounts with no parent, whose UID is not ROOT and whose type is not ROOT
return fetchAccounts("(" + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IS NULL OR "
+ AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " = ?) AND "
+ AccountEntry.COLUMN_HIDDEN + " = 0 AND "
+ AccountEntry.COLUMN_TYPE + " != ?",
new String[]{getOrCreateGnuCashRootAccountUID(), AccountType.ROOT.name()},
AccountEntry.COLUMN_NAME + " ASC");
}
/**
* Returns a cursor to accounts which have recently had transactions added to them
* @return Cursor to recently used accounts
*/
public Cursor fetchRecentAccounts(int numberOfRecent) {
return mDb.query(TransactionEntry.TABLE_NAME
+ " LEFT OUTER JOIN " + SplitEntry.TABLE_NAME + " ON "
+ TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " = "
+ SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID
+ " , " + AccountEntry.TABLE_NAME + " ON " + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID
+ " = " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID,
new String[]{AccountEntry.TABLE_NAME + ".*"},
AccountEntry.COLUMN_HIDDEN + " = 0",
null,
SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID, //groupby
null, //haveing
"MAX ( " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TIMESTAMP + " ) DESC", // order
Integer.toString(numberOfRecent) // limit;
);
}
/**
* Fetches favorite accounts from the database
* @return Cursor holding set of favorite accounts
*/
public Cursor fetchFavoriteAccounts(){
Log.v(LOG_TAG, "Fetching favorite accounts from db");
String condition = AccountEntry.COLUMN_FAVORITE + " = 1";
return mDb.query(AccountEntry.TABLE_NAME,
null, condition, null, null, null,
AccountEntry.COLUMN_NAME + " ASC");
}
/**
* Returns the GnuCash ROOT account UID if one exists (or creates one if necessary).
* <p>In GnuCash desktop account structure, there is a root account (which is not visible in the UI) from which
* other top level accounts derive. GnuCash Android also enforces a ROOT account now</p>
* @return Unique ID of the GnuCash root account.
*/
public String getOrCreateGnuCashRootAccountUID() {
Cursor cursor = fetchAccounts(AccountEntry.COLUMN_TYPE + "= ?",
new String[]{AccountType.ROOT.name()}, null);
try {
if (cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID));
}
} finally {
cursor.close();
}
// No ROOT exits, create a new one
Account rootAccount = new Account("ROOT Account", new CommoditiesDbAdapter(mDb).getCommodity("USD"));
rootAccount.setAccountType(AccountType.ROOT);
rootAccount.setFullName(ROOT_ACCOUNT_FULL_NAME);
rootAccount.setHidden(true);
rootAccount.setPlaceHolderFlag(true);
ContentValues contentValues = new ContentValues();
contentValues.put(AccountEntry.COLUMN_UID, rootAccount.getUID());
contentValues.put(AccountEntry.COLUMN_NAME, rootAccount.getName());
contentValues.put(AccountEntry.COLUMN_FULL_NAME, rootAccount.getFullName());
contentValues.put(AccountEntry.COLUMN_TYPE, rootAccount.getAccountType().name());
contentValues.put(AccountEntry.COLUMN_HIDDEN, rootAccount.isHidden() ? 1 : 0);
String defaultCurrencyCode = GnuCashApplication.getDefaultCurrencyCode();
contentValues.put(AccountEntry.COLUMN_CURRENCY, defaultCurrencyCode);
contentValues.put(AccountEntry.COLUMN_COMMODITY_UID, getCommodityUID(defaultCurrencyCode));
Log.i(LOG_TAG, "Creating ROOT account");
mDb.insert(AccountEntry.TABLE_NAME, null, contentValues);
return rootAccount.getUID();
}
/**
* Returns the number of accounts for which the account with ID <code>accoundId</code> is a first level parent
* @param accountUID String Unique ID (GUID) of the account
* @return Number of sub accounts
*/
public int getSubAccountCount(String accountUID){
//TODO: at some point when API level 11 and above only is supported, use DatabaseUtils.queryNumEntries
String queryCount = "SELECT COUNT(*) FROM " + AccountEntry.TABLE_NAME + " WHERE "
+ AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " = ?";
Cursor cursor = mDb.rawQuery(queryCount, new String[]{accountUID});
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return count;
}
/**
* Returns currency code of account with database ID <code>id</code>
* @param uid GUID of the account
* @return Currency code of the account
*/
public String getCurrencyCode(String uid){
return getAccountCurrencyCode(uid);
}
/**
* Returns the simple name of the account with unique ID <code>accountUID</code>.
* @param accountUID Unique identifier of the account
* @return Name of the account as String
* @throws java.lang.IllegalArgumentException if accountUID does not exist
* @see #getFullyQualifiedAccountName(String)
*/
public String getAccountName(String accountUID){
return getAttribute(accountUID, AccountEntry.COLUMN_NAME);
}
/**
* Returns the default transfer account record ID for the account with UID <code>accountUID</code>
* @param accountID Database ID of the account record
* @return Record ID of default transfer account
*/
public long getDefaultTransferAccountID(long accountID){
Cursor cursor = mDb.query(AccountEntry.TABLE_NAME,
new String[]{AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID},
AccountEntry._ID + " = " + accountID,
null, null, null, null);
try {
if (cursor.moveToNext()) {
String uid = cursor.getString(
cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID));
if (uid == null)
return 0;
else
return getID(uid);
} else {
return 0;
}
} finally {
cursor.close();
}
}
/**
* Returns the full account name including the account hierarchy (parent accounts)
* @param accountUID Unique ID of account
* @return Fully qualified (with parent hierarchy) account name
*/
public String getFullyQualifiedAccountName(String accountUID){
String accountName = getAccountName(accountUID);
String parentAccountUID = getParentAccountUID(accountUID);
if (parentAccountUID == null || parentAccountUID.equalsIgnoreCase(getOrCreateGnuCashRootAccountUID())){
return accountName;
}
String parentAccountName = getFullyQualifiedAccountName(parentAccountUID);
return parentAccountName + ACCOUNT_NAME_SEPARATOR + accountName;
}
/**
* get account's full name directly from DB
* @param accountUID the account to retrieve full name
* @return full name registered in DB
*/
public String getAccountFullName(String accountUID) {
Cursor cursor = mDb.query(AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_FULL_NAME},
AccountEntry.COLUMN_UID + " = ?", new String[]{accountUID},
null, null, null);
try {
if (cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_FULL_NAME));
}
}
finally {
cursor.close();
}
throw new IllegalArgumentException("account UID: " + accountUID + " does not exist");
}
/**
* Returns <code>true</code> if the account with unique ID <code>accountUID</code> is a placeholder account.
* @param accountUID Unique identifier of the account
* @return <code>true</code> if the account is a placeholder account, <code>false</code> otherwise
*/
public boolean isPlaceholderAccount(String accountUID) {
String isPlaceholder = getAttribute(accountUID, AccountEntry.COLUMN_PLACEHOLDER);
return Integer.parseInt(isPlaceholder) == 1;
}
/**
* Convenience method, resolves the account unique ID and calls {@link #isPlaceholderAccount(String)}
* @param accountUID GUID of the account
* @return <code>true</code> if the account is hidden, <code>false</code> otherwise
*/
public boolean isHiddenAccount(String accountUID){
String isHidden = getAttribute(accountUID, AccountEntry.COLUMN_HIDDEN);
return Integer.parseInt(isHidden) == 1;
}
/**
* Returns true if the account is a favorite account, false otherwise
* @param accountUID GUID of the account
* @return <code>true</code> if the account is a favorite account, <code>false</code> otherwise
*/
public boolean isFavoriteAccount(String accountUID){
String isFavorite = getAttribute(accountUID, AccountEntry.COLUMN_FAVORITE);
return Integer.parseInt(isFavorite) == 1;
}
/**
* Updates all opening balances to the current account balances
*/
public List<Transaction> getAllOpeningBalanceTransactions(){
Cursor cursor = fetchAccounts(null, null, null);
List<Transaction> openingTransactions = new ArrayList<>();
try {
SplitsDbAdapter splitsDbAdapter = mTransactionsAdapter.getSplitDbAdapter();
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(AccountEntry._ID));
String accountUID = getUID(id);
String currencyCode = getCurrencyCode(accountUID);
ArrayList<String> accountList = new ArrayList<>();
accountList.add(accountUID);
Money balance = splitsDbAdapter.computeSplitBalance(accountList,
currencyCode, getAccountType(accountUID).hasDebitNormalBalance());
if (balance.asBigDecimal().compareTo(new BigDecimal(0)) == 0)
continue;
Transaction transaction = new Transaction(GnuCashApplication.getAppContext().getString(R.string.account_name_opening_balances));
transaction.setNote(getAccountName(accountUID));
transaction.setCommodity(Commodity.getInstance(currencyCode));
TransactionType transactionType = Transaction.getTypeForBalance(getAccountType(accountUID),
balance.isNegative());
Split split = new Split(balance.abs(), accountUID);
split.setType(transactionType);
transaction.addSplit(split);
transaction.addSplit(split.createPair(getOrCreateOpeningBalanceAccountUID()));
transaction.setExported(true);
openingTransactions.add(transaction);
}
} finally {
cursor.close();
}
return openingTransactions;
}
public static String getImbalanceAccountPrefix() {
return GnuCashApplication.getAppContext().getString(R.string.imbalance_account_name) + "-";
}
/**
* Returns the imbalance account where to store transactions which are not double entry.
*
* @param commodity Commodity of the transaction
* @return Imbalance account name
*/
public static String getImbalanceAccountName(Commodity commodity){
return getImbalanceAccountPrefix() + commodity.getCurrencyCode();
}
/**
* Get the name of the default account for opening balances for the current locale.
* For the English locale, it will be "Equity:Opening Balances"
* @return Fully qualified account name of the opening balances account
*/
public static String getOpeningBalanceAccountFullName(){
Context context = GnuCashApplication.getAppContext();
String parentEquity = context.getString(R.string.account_name_equity).trim();
//German locale has no parent Equity account
if (parentEquity.length() > 0) {
return parentEquity + ACCOUNT_NAME_SEPARATOR
+ context.getString(R.string.account_name_opening_balances);
} else
return context.getString(R.string.account_name_opening_balances);
}
/**
* Returns the account color for the active account as an Android resource ID.
* <p>
* Basically, if we are in a top level account, use the default title color.
* but propagate a parent account's title color to children who don't have own color
* </p>
* @param accountUID GUID of the account
* @return Android resource ID representing the color which can be directly set to a view
*/
public static int getActiveAccountColorResource(@NonNull String accountUID) {
AccountsDbAdapter accountsDbAdapter = getInstance();
String colorCode = null;
int iColor = -1;
String parentAccountUID = accountUID;
while (parentAccountUID != null ) {
colorCode = accountsDbAdapter.getAccountColorCode(accountsDbAdapter.getID(parentAccountUID));
if (colorCode != null) {
iColor = Color.parseColor(colorCode);
break;
}
parentAccountUID = accountsDbAdapter.getParentAccountUID(parentAccountUID);
}
if (colorCode == null) {
iColor = GnuCashApplication.getAppContext().getResources().getColor(R.color.theme_primary);
}
return iColor;
}
/**
* Returns the list of commodities in use in the database.
*
* <p>This is not the same as the list of all available commodities.</p>
*
* @return List of commodities in use
*/
public List<Commodity> getCommoditiesInUse() {
Cursor cursor = mDb.query(true, AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_CURRENCY},
null, null, null, null, null, null);
List<Commodity> commodityList = new ArrayList<>();
try {
while (cursor.moveToNext()) {
String currencyCode =
cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY));
commodityList.add(mCommoditiesDbAdapter.getCommodity(currencyCode));
}
} finally {
cursor.close();
}
return commodityList;
}
/**
* Deletes all accounts, transactions (and their splits) from the database.
* Basically empties all 3 tables, so use with care ;)
*/
@Override
public int deleteAllRecords() {
// Relies "ON DELETE CASCADE" takes too much time
// It take more than 300s to complete the deletion on my dataset without
// clearing the split table first, but only needs a little more that 1s
// if the split table is cleared first.
mDb.delete(DatabaseSchema.PriceEntry.TABLE_NAME, null, null);
mDb.delete(SplitEntry.TABLE_NAME, null, null);
mDb.delete(TransactionEntry.TABLE_NAME, null, null);
mDb.delete(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, null, null);
mDb.delete(DatabaseSchema.BudgetAmountEntry.TABLE_NAME, null, null);
mDb.delete(DatabaseSchema.BudgetEntry.TABLE_NAME, null, null);
mDb.delete(DatabaseSchema.RecurrenceEntry.TABLE_NAME, null, null);
return mDb.delete(AccountEntry.TABLE_NAME, null, null);
}
@Override
public boolean deleteRecord(@NonNull String uid) {
boolean result = super.deleteRecord(uid);
if (result){
ContentValues contentValues = new ContentValues();
contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID);
mDb.update(mTableName, contentValues,
AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + "=?",
new String[]{uid});
}
return result;
}
public int getTransactionMaxSplitNum(@NonNull String accountUID) {
Cursor cursor = mDb.query("trans_extra_info",
new String[]{"MAX(trans_split_count)"},
"trans_acct_t_uid IN ( SELECT DISTINCT " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_UID +
" FROM trans_split_acct WHERE " + AccountEntry.TABLE_NAME + "_" + AccountEntry.COLUMN_UID +
" = ? )",
new String[]{accountUID},
null,
null,
null
);
try {
if (cursor.moveToFirst()) {
return (int)cursor.getLong(0);
} else {
return 0;
}
}
finally {
cursor.close();
}
}
}