/*
* Copyright (c) 2012 - 2015 Ngewi Fet <ngewif@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.ui.transaction;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.database.Cursor;
import android.inputmethodservice.KeyboardView;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SimpleCursorAdapter;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.FilterQueryProvider;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialogFragment;
import com.codetroopers.betterpickers.radialtimepicker.RadialTimePickerDialogFragment;
import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence;
import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter;
import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialogFragment;
import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.DatabaseSchema;
import org.gnucash.android.db.adapter.AccountsDbAdapter;
import org.gnucash.android.db.adapter.CommoditiesDbAdapter;
import org.gnucash.android.db.adapter.DatabaseAdapter;
import org.gnucash.android.db.adapter.PricesDbAdapter;
import org.gnucash.android.db.adapter.ScheduledActionDbAdapter;
import org.gnucash.android.db.adapter.TransactionsDbAdapter;
import org.gnucash.android.model.AccountType;
import org.gnucash.android.model.Commodity;
import org.gnucash.android.model.Money;
import org.gnucash.android.model.Recurrence;
import org.gnucash.android.model.ScheduledAction;
import org.gnucash.android.model.Split;
import org.gnucash.android.model.Transaction;
import org.gnucash.android.model.TransactionType;
import org.gnucash.android.ui.common.FormActivity;
import org.gnucash.android.ui.common.UxArgument;
import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity;
import org.gnucash.android.ui.settings.PreferenceActivity;
import org.gnucash.android.ui.transaction.dialog.TransferFundsDialogFragment;
import org.gnucash.android.ui.util.RecurrenceParser;
import org.gnucash.android.ui.util.RecurrenceViewClickListener;
import org.gnucash.android.ui.util.widget.CalculatorEditText;
import org.gnucash.android.ui.util.widget.TransactionTypeSwitch;
import org.gnucash.android.util.QualifiedAccountNameCursorAdapter;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* Fragment for creating or editing transactions
* @author Ngewi Fet <ngewif@gmail.com>
*/
public class TransactionFormFragment extends Fragment implements
CalendarDatePickerDialogFragment.OnDateSetListener, RadialTimePickerDialogFragment.OnTimeSetListener,
RecurrencePickerDialogFragment.OnRecurrenceSetListener, OnTransferFundsListener {
private static final int REQUEST_SPLIT_EDITOR = 0x11;
/**
* Transactions database adapter
*/
private TransactionsDbAdapter mTransactionsDbAdapter;
/**
* Accounts database adapter
*/
private AccountsDbAdapter mAccountsDbAdapter;
/**
* Adapter for transfer account spinner
*/
private QualifiedAccountNameCursorAdapter mAccountCursorAdapter;
/**
* Cursor for transfer account spinner
*/
private Cursor mCursor;
/**
* Transaction to be created/updated
*/
private Transaction mTransaction;
/**
* Formats a {@link Date} object into a date string of the format dd MMM yyyy e.g. 18 July 2012
*/
public final static DateFormat DATE_FORMATTER = DateFormat.getDateInstance();
/**
* Formats a {@link Date} object to time string of format HH:mm e.g. 15:25
*/
public final static DateFormat TIME_FORMATTER = DateFormat.getTimeInstance();
/**
* Button for setting the transaction type, either credit or debit
*/
@BindView(R.id.input_transaction_type) TransactionTypeSwitch mTransactionTypeSwitch;
/**
* Input field for the transaction name (description)
*/
@BindView(R.id.input_transaction_name) AutoCompleteTextView mDescriptionEditText;
/**
* Input field for the transaction amount
*/
@BindView(R.id.input_transaction_amount) CalculatorEditText mAmountEditText;
/**
* Field for the transaction currency.
* The transaction uses the currency of the account
*/
@BindView(R.id.currency_symbol) TextView mCurrencyTextView;
/**
* Input field for the transaction description (note)
*/
@BindView(R.id.input_description) EditText mNotesEditText;
/**
* Input field for the transaction date
*/
@BindView(R.id.input_date) TextView mDateTextView;
/**
* Input field for the transaction time
*/
@BindView(R.id.input_time) TextView mTimeTextView;
/**
* Spinner for selecting the transfer account
*/
@BindView(R.id.input_transfer_account_spinner) Spinner mTransferAccountSpinner;
/**
* Checkbox indicating if this transaction should be saved as a template or not
*/
@BindView(R.id.checkbox_save_template) CheckBox mSaveTemplateCheckbox;
@BindView(R.id.input_recurrence) TextView mRecurrenceTextView;
/**
* View which displays the calculator keyboard
*/
@BindView(R.id.calculator_keyboard) KeyboardView mKeyboardView;
/**
* Open the split editor
*/
@BindView(R.id.btn_split_editor) ImageView mOpenSplitEditor;
/**
* Layout for transfer account and associated views
*/
@BindView(R.id.layout_double_entry) View mDoubleEntryLayout;
/**
* Flag to note if double entry accounting is in use or not
*/
private boolean mUseDoubleEntry;
/**
* {@link Calendar} for holding the set date
*/
private Calendar mDate;
/**
* {@link Calendar} object holding the set time
*/
private Calendar mTime;
/**
* The AccountType of the account to which this transaction belongs.
* Used for determining the accounting rules for credits and debits
*/
AccountType mAccountType;
private String mRecurrenceRule;
private EventRecurrence mEventRecurrence = new EventRecurrence();
private String mAccountUID;
private List<Split> mSplitsList = new ArrayList<>();
private boolean mEditMode = false;
/**
* Split quantity which will be set from the funds transfer dialog
*/
private Money mSplitQuantity;
/**
* Create the view and retrieve references to the UI elements
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_transaction_form, container, false);
ButterKnife.bind(this, v);
mAmountEditText.bindListeners(mKeyboardView);
mOpenSplitEditor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openSplitEditor();
}
});
return v;
}
/**
* Starts the transfer of funds from one currency to another
*/
private void startTransferFunds() {
Commodity fromCommodity = Commodity.getInstance((mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID)));
long id = mTransferAccountSpinner.getSelectedItemId();
String targetCurrencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountsDbAdapter.getUID(id));
if (fromCommodity.equals(Commodity.getInstance(targetCurrencyCode))
|| !mAmountEditText.isInputModified()
|| mSplitQuantity != null) //if both accounts have same currency
return;
BigDecimal amountBigd = mAmountEditText.getValue();
if ((amountBigd == null) || amountBigd.equals(BigDecimal.ZERO))
return;
Money amount = new Money(amountBigd, fromCommodity).abs();
TransferFundsDialogFragment fragment
= TransferFundsDialogFragment.getInstance(amount, targetCurrencyCode, this);
fragment.show(getFragmentManager(), "transfer_funds_editor");
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mAmountEditText.bindListeners(mKeyboardView);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
SharedPreferences sharedPrefs = PreferenceActivity.getActiveBookSharedPreferences();
mUseDoubleEntry = sharedPrefs.getBoolean(getString(R.string.key_use_double_entry), false);
if (!mUseDoubleEntry){
mDoubleEntryLayout.setVisibility(View.GONE);
mOpenSplitEditor.setVisibility(View.GONE);
}
mAccountUID = getArguments().getString(UxArgument.SELECTED_ACCOUNT_UID);
assert(mAccountUID != null);
mAccountsDbAdapter = AccountsDbAdapter.getInstance();
mAccountType = mAccountsDbAdapter.getAccountType(mAccountUID);
String transactionUID = getArguments().getString(UxArgument.SELECTED_TRANSACTION_UID);
mTransactionsDbAdapter = TransactionsDbAdapter.getInstance();
if (transactionUID != null) {
mTransaction = mTransactionsDbAdapter.getRecord(transactionUID);
}
setListeners();
//updateTransferAccountsList must only be called after initializing mAccountsDbAdapter
updateTransferAccountsList();
mTransferAccountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
/**
* Flag for ignoring first call to this listener.
* The first call is during layout, but we want it called only in response to user interaction
*/
boolean userInteraction = false;
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// Remove the favorite star from the view to avoid visual clutter.
TextView qualifiedAccountName = (TextView) view;
qualifiedAccountName.setCompoundDrawablesWithIntrinsicBounds(0,0,0,0);
if (mSplitsList.size() == 2) { //when handling simple transfer to one account
for (Split split : mSplitsList) {
if (!split.getAccountUID().equals(mAccountUID)) {
split.setAccountUID(mAccountsDbAdapter.getUID(id));
}
// else case is handled when saving the transactions
}
}
if (!userInteraction) {
userInteraction = true;
return;
}
startTransferFunds();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
//nothing to see here, move along
}
});
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
assert actionBar != null;
// actionBar.setSubtitle(mAccountsDbAdapter.getFullyQualifiedAccountName(mAccountUID));
if (mTransaction == null) {
actionBar.setTitle(R.string.title_add_transaction);
initalizeViews();
initTransactionNameAutocomplete();
} else {
actionBar.setTitle(R.string.title_edit_transaction);
initializeViewsWithTransaction();
mEditMode = true;
}
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
/**
* Extension of SimpleCursorAdapter which is used to populate the fields for the list items
* in the transactions suggestions (auto-complete transaction description).
*/
private class DropDownCursorAdapter extends SimpleCursorAdapter{
public DropDownCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c, from, to, 0);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
super.bindView(view, context, cursor);
String transactionUID = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_UID));
Money balance = TransactionsDbAdapter.getInstance().getBalance(transactionUID, mAccountUID);
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_TIMESTAMP));
String dateString = DateUtils.formatDateTime(getActivity(), timestamp,
DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR);
TextView secondaryTextView = (TextView) view.findViewById(R.id.secondary_text);
secondaryTextView.setText(balance.formattedString() + " on " + dateString); //TODO: Extract string
}
}
/**
* Initializes the transaction name field for autocompletion with existing transaction names in the database
*/
private void initTransactionNameAutocomplete() {
final int[] to = new int[]{R.id.primary_text};
final String[] from = new String[]{DatabaseSchema.TransactionEntry.COLUMN_DESCRIPTION};
SimpleCursorAdapter adapter = new DropDownCursorAdapter(
getActivity(), R.layout.dropdown_item_2lines, null, from, to);
adapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor cursor) {
final int colIndex = cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_DESCRIPTION);
return cursor.getString(colIndex);
}
});
adapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence name) {
return mTransactionsDbAdapter.fetchTransactionSuggestions(name == null ? "" : name.toString(), mAccountUID);
}
});
mDescriptionEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
mTransaction = new Transaction(mTransactionsDbAdapter.getRecord(id), true);
mTransaction.setTime(System.currentTimeMillis());
//we check here because next method will modify it and we want to catch user-modification
boolean amountEntered = mAmountEditText.isInputModified();
initializeViewsWithTransaction();
List<Split> splitList = mTransaction.getSplits();
boolean isSplitPair = splitList.size() == 2 && splitList.get(0).isPairOf(splitList.get(1));
if (isSplitPair){
mSplitsList.clear();
if (!amountEntered) //if user already entered an amount
mAmountEditText.setValue(splitList.get(0).getValue().asBigDecimal());
} else {
if (amountEntered){ //if user entered own amount, clear loaded splits and use the user value
mSplitsList.clear();
setDoubleEntryViewsVisibility(View.VISIBLE);
} else {
if (mUseDoubleEntry) { //don't hide the view in single entry mode
setDoubleEntryViewsVisibility(View.GONE);
}
}
}
mTransaction = null; //we are creating a new transaction after all
}
});
mDescriptionEditText.setAdapter(adapter);
}
/**
* Initialize views in the fragment with information from a transaction.
* This method is called if the fragment is used for editing a transaction
*/
private void initializeViewsWithTransaction(){
mDescriptionEditText.setText(mTransaction.getDescription());
mDescriptionEditText.setSelection(mDescriptionEditText.getText().length());
mTransactionTypeSwitch.setAccountType(mAccountType);
mTransactionTypeSwitch.setChecked(mTransaction.getBalance(mAccountUID).isNegative());
if (!mAmountEditText.isInputModified()){
//when autocompleting, only change the amount if the user has not manually changed it already
mAmountEditText.setValue(mTransaction.getBalance(mAccountUID).asBigDecimal());
}
mCurrencyTextView.setText(mTransaction.getCommodity().getSymbol());
mNotesEditText.setText(mTransaction.getNote());
mDateTextView.setText(DATE_FORMATTER.format(mTransaction.getTimeMillis()));
mTimeTextView.setText(TIME_FORMATTER.format(mTransaction.getTimeMillis()));
Calendar cal = GregorianCalendar.getInstance();
cal.setTimeInMillis(mTransaction.getTimeMillis());
mDate = mTime = cal;
//TODO: deep copy the split list. We need a copy so we can modify with impunity
mSplitsList = new ArrayList<>(mTransaction.getSplits());
toggleAmountInputEntryMode(mSplitsList.size() <= 2);
if (mSplitsList.size() == 2){
for (Split split : mSplitsList) {
if (split.getAccountUID().equals(mAccountUID)) {
if (!split.getQuantity().getCommodity().equals(mTransaction.getCommodity())){
mSplitQuantity = split.getQuantity();
}
}
}
}
//if there are more than two splits (which is the default for one entry), then
//disable editing of the transfer account. User should open editor
if (mSplitsList.size() == 2 && mSplitsList.get(0).isPairOf(mSplitsList.get(1))) {
for (Split split : mTransaction.getSplits()) {
//two splits, one belongs to this account and the other to another account
if (mUseDoubleEntry && !split.getAccountUID().equals(mAccountUID)) {
setSelectedTransferAccount(mAccountsDbAdapter.getID(split.getAccountUID()));
}
}
} else {
setDoubleEntryViewsVisibility(View.GONE);
}
String currencyCode = mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID);
Commodity accountCommodity = Commodity.getInstance(currencyCode);
mCurrencyTextView.setText(accountCommodity.getSymbol());
Commodity commodity = Commodity.getInstance(currencyCode);
mAmountEditText.setCommodity(commodity);
mSaveTemplateCheckbox.setChecked(mTransaction.isTemplate());
String scheduledActionUID = getArguments().getString(UxArgument.SCHEDULED_ACTION_UID);
if (scheduledActionUID != null && !scheduledActionUID.isEmpty()) {
ScheduledAction scheduledAction = ScheduledActionDbAdapter.getInstance().getRecord(scheduledActionUID);
mRecurrenceRule = scheduledAction.getRuleString();
mEventRecurrence.parse(mRecurrenceRule);
mRecurrenceTextView.setText(scheduledAction.getRepeatString());
}
}
private void setDoubleEntryViewsVisibility(int visibility) {
mDoubleEntryLayout.setVisibility(visibility);
mTransactionTypeSwitch.setVisibility(visibility);
}
private void toggleAmountInputEntryMode(boolean enabled){
if (enabled){
mAmountEditText.setFocusable(true);
mAmountEditText.bindListeners(mKeyboardView);
} else {
mAmountEditText.setFocusable(false);
mAmountEditText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openSplitEditor();
}
});
}
}
/**
* Initialize views with default data for new transactions
*/
private void initalizeViews() {
Date time = new Date(System.currentTimeMillis());
mDateTextView.setText(DATE_FORMATTER.format(time));
mTimeTextView.setText(TIME_FORMATTER.format(time));
mTime = mDate = Calendar.getInstance();
mTransactionTypeSwitch.setAccountType(mAccountType);
String typePref = PreferenceActivity.getActiveBookSharedPreferences().getString(getString(R.string.key_default_transaction_type), "DEBIT");
mTransactionTypeSwitch.setChecked(TransactionType.valueOf(typePref));
String code = GnuCashApplication.getDefaultCurrencyCode();
if (mAccountUID != null){
code = mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID);
}
Commodity commodity = Commodity.getInstance(code);
mCurrencyTextView.setText(commodity.getSymbol());
mAmountEditText.setCommodity(commodity);
if (mUseDoubleEntry){
String currentAccountUID = mAccountUID;
long defaultTransferAccountID;
String rootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID();
do {
defaultTransferAccountID = mAccountsDbAdapter.getDefaultTransferAccountID(mAccountsDbAdapter.getID(currentAccountUID));
if (defaultTransferAccountID > 0) {
setSelectedTransferAccount(defaultTransferAccountID);
break; //we found a parent with default transfer setting
}
currentAccountUID = mAccountsDbAdapter.getParentAccountUID(currentAccountUID);
} while (!currentAccountUID.equals(rootAccountUID));
}
}
/**
* Updates the list of possible transfer accounts.
* Only accounts with the same currency can be transferred to
*/
private void updateTransferAccountsList(){
String conditions = "(" + DatabaseSchema.AccountEntry.COLUMN_UID + " != ?"
+ " AND " + DatabaseSchema.AccountEntry.COLUMN_TYPE + " != ?"
+ " AND " + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0"
+ ")";
if (mCursor != null) {
mCursor.close();
}
mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(conditions, new String[]{mAccountUID, AccountType.ROOT.name()});
mAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor);
mTransferAccountSpinner.setAdapter(mAccountCursorAdapter);
}
/**
* Opens the split editor dialog
*/
private void openSplitEditor(){
if (mAmountEditText.getValue() == null){
Toast.makeText(getActivity(), R.string.toast_enter_amount_to_split, Toast.LENGTH_SHORT).show();
return;
}
String baseAmountString;
if (mTransaction == null){ //if we are creating a new transaction (not editing an existing one)
BigDecimal enteredAmount = mAmountEditText.getValue();
baseAmountString = enteredAmount.toPlainString();
} else {
Money biggestAmount = Money.createZeroInstance(mTransaction.getCurrencyCode());
for (Split split : mTransaction.getSplits()) {
if (split.getValue().asBigDecimal().compareTo(biggestAmount.asBigDecimal()) > 0)
biggestAmount = split.getValue();
}
baseAmountString = biggestAmount.toPlainString();
}
Intent intent = new Intent(getActivity(), FormActivity.class);
intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.SPLIT_EDITOR.name());
intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID);
intent.putExtra(UxArgument.AMOUNT_STRING, baseAmountString);
intent.putParcelableArrayListExtra(UxArgument.SPLIT_LIST, (ArrayList<Split>) extractSplitsFromView());
startActivityForResult(intent, REQUEST_SPLIT_EDITOR);
}
/**
* Sets click listeners for the dialog buttons
*/
private void setListeners() {
mTransactionTypeSwitch.setAmountFormattingListener(mAmountEditText, mCurrencyTextView);
mDateTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long dateMillis = 0;
try {
Date date = DATE_FORMATTER.parse(mDateTextView.getText().toString());
dateMillis = date.getTime();
} catch (ParseException e) {
Log.e(getTag(), "Error converting input time to Date object");
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(dateMillis);
int year = calendar.get(Calendar.YEAR);
int monthOfYear = calendar.get(Calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
CalendarDatePickerDialogFragment datePickerDialog = new CalendarDatePickerDialogFragment()
.setOnDateSetListener(TransactionFormFragment.this)
.setPreselectedDate(year, monthOfYear, dayOfMonth);
datePickerDialog.show(getFragmentManager(), "date_picker_fragment");
}
});
mTimeTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long timeMillis = 0;
try {
Date date = TIME_FORMATTER.parse(mTimeTextView.getText().toString());
timeMillis = date.getTime();
} catch (ParseException e) {
Log.e(getTag(), "Error converting input time to Date object");
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timeMillis);
RadialTimePickerDialogFragment timePickerDialog = new RadialTimePickerDialogFragment()
.setOnTimeSetListener(TransactionFormFragment.this)
.setStartTime(calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE));
timePickerDialog.show(getFragmentManager(), "time_picker_dialog_fragment");
}
});
mRecurrenceTextView.setOnClickListener(new RecurrenceViewClickListener((AppCompatActivity) getActivity(), mRecurrenceRule, this));
}
/**
* Updates the spinner to the selected transfer account
* @param accountId Database ID of the transfer account
*/
private void setSelectedTransferAccount(long accountId){
int position = mAccountCursorAdapter.getPosition(mAccountsDbAdapter.getUID(accountId));
if (position >= 0)
mTransferAccountSpinner.setSelection(position);
}
/**
* Returns a list of splits based on the input in the transaction form.
* This only gets the splits from the simple view, and not those from the Split Editor.
* If the Split Editor has been used and there is more than one split, then it returns {@link #mSplitsList}
* @return List of splits in the view or {@link #mSplitsList} is there are more than 2 splits in the transaction
*/
private List<Split> extractSplitsFromView(){
if (mTransactionTypeSwitch.getVisibility() != View.VISIBLE){
return mSplitsList;
}
BigDecimal amountBigd = mAmountEditText.getValue();
String baseCurrencyCode = mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID);
Money value = new Money(amountBigd, Commodity.getInstance(baseCurrencyCode)).abs();
Money quantity = new Money(value);
String transferAcctUID = getTransferAccountUID();
CommoditiesDbAdapter cmdtyDbAdapter = CommoditiesDbAdapter.getInstance();
if (isMultiCurrencyTransaction()){ //if multi-currency transaction
String transferCurrencyCode = mAccountsDbAdapter.getCurrencyCode(transferAcctUID);
String commodityUID = cmdtyDbAdapter.getCommodityUID(baseCurrencyCode);
String targetCmdtyUID = cmdtyDbAdapter.getCommodityUID(transferCurrencyCode);
Pair<Long, Long> pricePair = PricesDbAdapter.getInstance()
.getPrice(commodityUID, targetCmdtyUID);
if (pricePair.first > 0 && pricePair.second > 0) {
quantity = quantity.multiply(pricePair.first.intValue())
.divide(pricePair.second.intValue())
.withCurrency(cmdtyDbAdapter.getRecord(targetCmdtyUID));
}
}
Split split1 = new Split(value, mAccountUID);
split1.setType(mTransactionTypeSwitch.getTransactionType());
Split split2 = new Split(value, quantity, transferAcctUID);
split2.setType(mTransactionTypeSwitch.getTransactionType().invert());
List<Split> splitList = new ArrayList<>();
splitList.add(split1);
splitList.add(split2);
return splitList;
}
/**
* Returns the GUID of the currently selected transfer account.
* If double-entry is disabled, this method returns the GUID of the imbalance account for the currently active account
* @return GUID of transfer account
*/
private @NonNull String getTransferAccountUID() {
String transferAcctUID;
if (mUseDoubleEntry) {
long transferAcctId = mTransferAccountSpinner.getSelectedItemId();
transferAcctUID = mAccountsDbAdapter.getUID(transferAcctId);
} else {
Commodity baseCommodity = mAccountsDbAdapter.getRecord(mAccountUID).getCommodity();
transferAcctUID = mAccountsDbAdapter.getOrCreateImbalanceAccountUID(baseCommodity);
}
return transferAcctUID;
}
/**
* Extracts a transaction from the input in the form fragment
* @return New transaction object containing all info in the form
*/
private @NonNull Transaction extractTransactionFromView(){
Calendar cal = new GregorianCalendar(
mDate.get(Calendar.YEAR),
mDate.get(Calendar.MONTH),
mDate.get(Calendar.DAY_OF_MONTH),
mTime.get(Calendar.HOUR_OF_DAY),
mTime.get(Calendar.MINUTE),
mTime.get(Calendar.SECOND));
String description = mDescriptionEditText.getText().toString();
String notes = mNotesEditText.getText().toString();
String currencyCode = mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID);
Commodity commodity = CommoditiesDbAdapter.getInstance().getCommodity(currencyCode);
List<Split> splits = extractSplitsFromView();
Transaction transaction = new Transaction(description);
transaction.setTime(cal.getTimeInMillis());
transaction.setCommodity(commodity);
transaction.setNote(notes);
transaction.setSplits(splits);
transaction.setExported(false); //not necessary as exports use timestamps now. Because, legacy
return transaction;
}
/**
* Checks whether the split editor has been used for editing this transaction.
* <p>The Split Editor is considered to have been used if the transaction type switch is not visible</p>
* @return {@code true} if split editor was used, {@code false} otherwise
*/
private boolean splitEditorUsed(){
return mTransactionTypeSwitch.getVisibility() != View.VISIBLE;
}
/**
* Checks if this is a multi-currency transaction being created/edited
* <p>A multi-currency transaction is one in which the main account and transfer account have different currencies. <br>
* Single-entry transactions cannot be multi-currency</p>
* @return {@code true} if multi-currency transaction, {@code false} otherwise
*/
private boolean isMultiCurrencyTransaction(){
if (!mUseDoubleEntry)
return false;
String transferAcctUID = mAccountsDbAdapter.getUID(mTransferAccountSpinner.getSelectedItemId());
String currencyCode = mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID);
String transferCurrencyCode = mAccountsDbAdapter.getCurrencyCode(transferAcctUID);
return !currencyCode.equals(transferCurrencyCode);
}
/**
* Collects information from the fragment views and uses it to create
* and save a transaction
*/
private void saveNewTransaction() {
mAmountEditText.getCalculatorKeyboard().hideCustomKeyboard();
//determine whether we need to do currency conversion
if (isMultiCurrencyTransaction() && !splitEditorUsed() && !mCurrencyConversionDone){
startTransferFunds();
return;
}
Transaction transaction = extractTransactionFromView();
if (mEditMode) { //if editing an existing transaction
transaction.setUID(mTransaction.getUID());
}
mTransaction = transaction;
mAccountsDbAdapter.beginTransaction();
try {
// 1) mTransactions may be existing or non-existing
// 2) when mTransactions exists in the db, the splits may exist or not exist in the db
// So replace is chosen.
mTransactionsDbAdapter.addRecord(mTransaction, DatabaseAdapter.UpdateMethod.replace);
if (mSaveTemplateCheckbox.isChecked()) {//template is automatically checked when a transaction is scheduled
if (!mEditMode) { //means it was new transaction, so a new template
Transaction templateTransaction = new Transaction(mTransaction, true);
templateTransaction.setTemplate(true);
mTransactionsDbAdapter.addRecord(templateTransaction, DatabaseAdapter.UpdateMethod.replace);
scheduleRecurringTransaction(templateTransaction.getUID());
} else
scheduleRecurringTransaction(mTransaction.getUID());
} else {
String scheduledActionUID = getArguments().getString(UxArgument.SCHEDULED_ACTION_UID);
if (scheduledActionUID != null){ //we were editing a schedule and it was turned off
ScheduledActionDbAdapter.getInstance().deleteRecord(scheduledActionUID);
}
}
mAccountsDbAdapter.setTransactionSuccessful();
}
finally {
mAccountsDbAdapter.endTransaction();
}
//update widgets, if any
WidgetConfigurationActivity.updateAllWidgets(getActivity().getApplicationContext());
finish(Activity.RESULT_OK);
}
/**
* Schedules a recurring transaction (if necessary) after the transaction has been saved
* @see #saveNewTransaction()
*/
private void scheduleRecurringTransaction(String transactionUID) {
ScheduledActionDbAdapter scheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance();
Recurrence recurrence = RecurrenceParser.parse(mEventRecurrence);
ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
scheduledAction.setRecurrence(recurrence);
String scheduledActionUID = getArguments().getString(UxArgument.SCHEDULED_ACTION_UID);
if (scheduledActionUID != null) { //if we are editing an existing schedule
if (recurrence == null){
scheduledActionDbAdapter.deleteRecord(scheduledActionUID);
} else {
scheduledAction.setUID(scheduledActionUID);
scheduledActionDbAdapter.updateRecurrenceAttributes(scheduledAction);
Toast.makeText(getActivity(), R.string.toast_updated_transaction_recurring_schedule, Toast.LENGTH_SHORT).show();
}
} else {
if (recurrence != null) {
scheduledAction.setActionUID(transactionUID);
scheduledActionDbAdapter.addRecord(scheduledAction, DatabaseAdapter.UpdateMethod.replace);
Toast.makeText(getActivity(), R.string.toast_scheduled_recurring_transaction, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (mCursor != null)
mCursor.close();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.default_save_actions, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
//hide the keyboard if it is visible
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mDescriptionEditText.getApplicationWindowToken(), 0);
switch (item.getItemId()) {
case android.R.id.home:
finish(Activity.RESULT_CANCELED);
return true;
case R.id.menu_save:
if (canSave()){
saveNewTransaction();
} else {
if (mAmountEditText.getValue() == null) {
Toast.makeText(getActivity(), R.string.toast_transanction_amount_required, Toast.LENGTH_SHORT).show();
}
if (mUseDoubleEntry && mTransferAccountSpinner.getCount() == 0){
Toast.makeText(getActivity(),
R.string.toast_disable_double_entry_to_save_transaction,
Toast.LENGTH_LONG).show();
}
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Checks if the pre-requisites for saving the transaction are fulfilled
* <p>The conditions checked are that a valid amount is entered and that a transfer account is set (where applicable)</p>
* @return {@code true} if the transaction can be saved, {@code false} otherwise
*/
private boolean canSave(){
return (mUseDoubleEntry && mAmountEditText.isInputValid()
&& mTransferAccountSpinner.getCount() > 0)
|| (!mUseDoubleEntry && mAmountEditText.isInputValid());
}
/**
* Called by the split editor fragment to notify of finished editing
* @param splitList List of splits produced in the fragment
*/
public void setSplitList(List<Split> splitList){
mSplitsList = splitList;
Money balance = Transaction.computeBalance(mAccountUID, mSplitsList);
mAmountEditText.setValue(balance.asBigDecimal());
mTransactionTypeSwitch.setChecked(balance.isNegative());
}
/**
* Finishes the fragment appropriately.
* Depends on how the fragment was loaded, it might have a backstack or not
*/
private void finish(int resultCode) {
if (getActivity().getSupportFragmentManager().getBackStackEntryCount() == 0){
getActivity().setResult(resultCode);
//means we got here directly from the accounts list activity, need to finish
getActivity().finish();
} else {
//go back to transactions list
getActivity().getSupportFragmentManager().popBackStack();
}
}
@Override
public void onDateSet(CalendarDatePickerDialogFragment calendarDatePickerDialog, int year, int monthOfYear, int dayOfMonth) {
Calendar cal = new GregorianCalendar(year, monthOfYear, dayOfMonth);
mDateTextView.setText(DATE_FORMATTER.format(cal.getTime()));
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, monthOfYear);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
}
@Override
public void onTimeSet(RadialTimePickerDialogFragment radialTimePickerDialog, int hourOfDay, int minute) {
Calendar cal = new GregorianCalendar(0, 0, 0, hourOfDay, minute);
mTimeTextView.setText(TIME_FORMATTER.format(cal.getTime()));
mTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
mTime.set(Calendar.MINUTE, minute);
}
/**
* Strips formatting from a currency string.
* All non-digit information is removed, but the sign is preserved.
* @param s String to be stripped
* @return Stripped string with all non-digits removed
*/
public static String stripCurrencyFormatting(String s){
if (s.length() == 0)
return s;
//remove all currency formatting and anything else which is not a number
String sign = s.trim().substring(0,1);
String stripped = s.trim().replaceAll("\\D*", "");
if (stripped.length() == 0)
return "";
if (sign.equals("+") || sign.equals("-")){
stripped = sign + stripped;
}
return stripped;
}
/**
* Flag for checking where the TransferFunds dialog has already been displayed to the user
*/
boolean mCurrencyConversionDone = false;
@Override
public void transferComplete(Money amount) {
mCurrencyConversionDone = true;
mSplitQuantity = amount;
}
@Override
public void onRecurrenceSet(String rrule) {
mRecurrenceRule = rrule;
String repeatString = getString(R.string.label_tap_to_create_schedule);
if (mRecurrenceRule != null){
mEventRecurrence.parse(mRecurrenceRule);
repeatString = EventRecurrenceFormatter.getRepeatString(getActivity(), getResources(), mEventRecurrence, true);
//when recurrence is set, we will definitely be saving a template
mSaveTemplateCheckbox.setChecked(true);
mSaveTemplateCheckbox.setEnabled(false);
} else {
mSaveTemplateCheckbox.setEnabled(true);
mSaveTemplateCheckbox.setChecked(false);
}
mRecurrenceTextView.setText(repeatString);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK){
List<Split> splitList = data.getParcelableArrayListExtra(UxArgument.SPLIT_LIST);
setSplitList(splitList);
//once split editor has been used and saved, only allow editing through it
toggleAmountInputEntryMode(false);
setDoubleEntryViewsVisibility(View.GONE);
mOpenSplitEditor.setVisibility(View.VISIBLE);
}
}
}