/* * Copyright (c) 2012 - 2014 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.model; import android.content.Intent; import android.support.annotation.NonNull; import org.gnucash.android.BuildConfig; import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.export.ofx.OfxHelper; import org.gnucash.android.model.Account.OfxAccountType; import org.w3c.dom.Document; import org.w3c.dom.Element; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Represents a financial transaction, either credit or debit. * Transactions belong to accounts and each have the unique identifier of the account to which they belong. * The default type is a debit, unless otherwise specified. * @author Ngewi Fet <ngewif@gmail.com> */ public class Transaction extends BaseModel{ /** * Mime type for transactions in Gnucash. * Used for recording transactions through intents */ public static final String MIME_TYPE = "vnd.android.cursor.item/vnd." + BuildConfig.APPLICATION_ID + ".transaction"; /** * Key for passing the account unique Identifier as an argument through an {@link Intent} * @deprecated use {@link Split}s instead */ @Deprecated public static final String EXTRA_ACCOUNT_UID = "org.gnucash.android.extra.account_uid"; /** * Key for specifying the double entry account * @deprecated use {@link Split}s instead */ @Deprecated public static final String EXTRA_DOUBLE_ACCOUNT_UID = "org.gnucash.android.extra.double_account_uid"; /** * Key for identifying the amount of the transaction through an Intent * @deprecated use {@link Split}s instead */ @Deprecated public static final String EXTRA_AMOUNT = "org.gnucash.android.extra.amount"; /** * Extra key for the transaction type. * This value should typically be set by calling {@link TransactionType#name()} * @deprecated use {@link Split}s instead */ @Deprecated public static final String EXTRA_TRANSACTION_TYPE = "org.gnucash.android.extra.transaction_type"; /** * Argument key for passing splits as comma-separated multi-line list and each line is a split. * The line format is: <type>;<amount>;<account_uid> * The amount should be formatted in the US Locale */ public static final String EXTRA_SPLITS = "org.gnucash.android.extra.transaction.splits"; /** * GUID of commodity associated with this transaction */ private Commodity mCommodity; /** * The splits making up this transaction */ private List<Split> mSplitList = new ArrayList<>(); /** * Name describing the transaction */ private String mDescription; /** * An extra note giving details about the transaction */ private String mNotes = ""; /** * Flag indicating if this transaction has been exported before or not * The transactions are typically exported as bank statement in the OFX format */ private boolean mIsExported = false; /** * Timestamp when this transaction occurred */ private long mTimestamp; /** * Flag indicating that this transaction is a template */ private boolean mIsTemplate = false; /** * GUID of ScheduledAction which created this transaction */ private String mScheduledActionUID = null; /** * Overloaded constructor. Creates a new transaction instance with the * provided data and initializes the rest to default values. * @param name Name of the transaction */ public Transaction(String name) { initDefaults(); setDescription(name); } /** * Copy constructor. * Creates a new transaction object which is a clone of the parameter. * <p><b>Note:</b> The unique ID of the transaction is not cloned if the parameter <code>generateNewUID</code>, * is set to false. Otherwise, a new one is generated.<br/> * The export flag and the template flag are not copied from the old transaction to the new.</p> * @param transaction Transaction to be cloned * @param generateNewUID Flag to determine if new UID should be assigned or not */ public Transaction(Transaction transaction, boolean generateNewUID){ initDefaults(); setDescription(transaction.getDescription()); setNote(transaction.getNote()); setTime(transaction.getTimeMillis()); setCommodity(transaction.getCommodity()); //exported flag is left at default value of false for (Split split : transaction.mSplitList) { addSplit(new Split(split, generateNewUID)); } if (!generateNewUID){ setUID(transaction.getUID()); } } /** * Initializes the different fields to their default values. */ private void initDefaults(){ setCommodity(Commodity.DEFAULT_COMMODITY); this.mTimestamp = System.currentTimeMillis(); } /** * Creates a split which will balance the transaction, in value. * <p><b>Note:</b>If a transaction has splits with different currencies, no auto-balancing will be performed.</p> * * <p>The added split will not use any account in db, but will use currency code as account UID. * The added split will be returned, to be filled with proper account UID later.</p> * @return Split whose amount is the imbalance of this transaction */ public Split createAutoBalanceSplit(){ Money imbalance = getImbalance(); //returns imbalance of 0 for multicurrency transactions if (!imbalance.isAmountZero()){ // yes, this is on purpose the account UID is set to the currency. // This should be overridden before saving to db Split split = new Split(imbalance.negate(), mCommodity.getCurrencyCode()); addSplit(split); return split; } return null; } /** * Set the GUID of the transaction * If the transaction has Splits, their transactionGUID will be updated as well * @param uid String unique ID */ @Override public void setUID(String uid) { super.setUID(uid); for (Split split : mSplitList) { split.setTransactionUID(uid); } } /** * Returns list of splits for this transaction * @return {@link java.util.List} of splits in the transaction */ public List<Split> getSplits(){ return mSplitList; } /** * Returns the list of splits belonging to a specific account * @param accountUID Unique Identifier of the account * @return List of {@link org.gnucash.android.model.Split}s */ public List<Split> getSplits(String accountUID){ List<Split> splits = new ArrayList<>(); for (Split split : mSplitList) { if (split.getAccountUID().equals(accountUID)){ splits.add(split); } } return splits; } /** * Sets the splits for this transaction * <p>All the splits in the list will have their transaction UID set to this transaction</p> * @param splitList List of splits for this transaction */ public void setSplits(List<Split> splitList){ mSplitList = splitList; for (Split split : splitList) { split.setTransactionUID(getUID()); } } /** * Add a split to the transaction. * <p>Sets the split UID and currency to that of this transaction</p> * @param split Split for this transaction */ public void addSplit(Split split){ //sets the currency of the split to the currency of the transaction split.setTransactionUID(getUID()); mSplitList.add(split); } /** * Returns the balance of this transaction for only those splits which relate to the account. * <p>Uses a call to {@link #getBalance(String)} with the appropriate parameters</p> * @param accountUID Unique Identifier of the account * @return Money balance of the transaction for the specified account * @see #computeBalance(String, java.util.List) */ public Money getBalance(String accountUID){ return computeBalance(accountUID, mSplitList); } /** * Computes the imbalance amount for the given transaction. * In double entry, all transactions should resolve to zero. But imbalance occurs when there are unresolved splits. * <p><b>Note:</b> If this is a multi-currency transaction, an imbalance of zero will be returned</p> * @return Money imbalance of the transaction or zero if it is a multi-currency transaction */ public Money getImbalance(){ Money imbalance = Money.createZeroInstance(mCommodity.getCurrencyCode()); for (Split split : mSplitList) { if (!split.getQuantity().getCommodity().equals(mCommodity)) { // this may happen when importing XML exported from GNCA before 2.0.0 // these transactions should only be imported from XML exported from GNC desktop // so imbalance split should not be generated for them return Money.createZeroInstance(mCommodity.getCurrencyCode()); } Money amount = split.getValue().abs(); if (split.getType() == TransactionType.DEBIT) imbalance = imbalance.subtract(amount); else imbalance = imbalance.add(amount); } return imbalance; } /** * Computes the balance of the splits belonging to a particular account. * <p>Only those splits which belong to the account will be considered. * If the {@code accountUID} is null, then the imbalance of the transaction is computed. This means that either * zero is returned (for balanced transactions) or the imbalance amount will be returned.</p> * @param accountUID Unique Identifier of the account * @param splitList List of splits * @return Money list of splits */ public static Money computeBalance(String accountUID, List<Split> splitList) { AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); AccountType accountType = accountsDbAdapter.getAccountType(accountUID); String accountCurrencyCode = accountsDbAdapter.getAccountCurrencyCode(accountUID); boolean isDebitAccount = accountType.hasDebitNormalBalance(); Money balance = Money.createZeroInstance(accountCurrencyCode); for (Split split : splitList) { if (!split.getAccountUID().equals(accountUID)) continue; Money absAmount; if (split.getValue().getCommodity().getCurrencyCode().equals(accountCurrencyCode)){ absAmount = split.getValue().abs(); } else { //if this split belongs to the account, then either its value or quantity is in the account currency absAmount = split.getQuantity().abs(); } boolean isDebitSplit = split.getType() == TransactionType.DEBIT; if (isDebitAccount) { if (isDebitSplit) { balance = balance.add(absAmount); } else { balance = balance.subtract(absAmount); } } else { if (isDebitSplit) { balance = balance.subtract(absAmount); } else { balance = balance.add(absAmount); } } } return balance; } /** * Returns the currency code of this transaction. * @return ISO 4217 currency code string */ public String getCurrencyCode() { return mCommodity.getCurrencyCode(); } /** * Returns the commodity for this transaction * @return Commodity of the transaction */ public @NonNull Commodity getCommodity() { return mCommodity; } /** * Sets the commodity for this transaction * @param commodity Commodity instance */ public void setCommodity(@NonNull Commodity commodity) { this.mCommodity = commodity; } /** * Returns the description of the transaction * @return Transaction description */ public String getDescription() { return mDescription; } /** * Sets the transaction description * @param description String description */ public void setDescription(String description) { this.mDescription = description.trim(); } /** * Add notes to the transaction * @param notes String containing notes for the transaction */ public void setNote(String notes) { this.mNotes = notes; } /** * Returns the transaction notes * @return String notes of transaction */ public String getNote() { return mNotes; } /** * Set the time of the transaction * @param timestamp Time when transaction occurred as {@link Date} */ public void setTime(Date timestamp){ this.mTimestamp = timestamp.getTime(); } /** * Sets the time when the transaction occurred * @param timeInMillis Time in milliseconds */ public void setTime(long timeInMillis) { this.mTimestamp = timeInMillis; } /** * Returns the time of transaction in milliseconds * @return Time when transaction occurred in milliseconds */ public long getTimeMillis(){ return mTimestamp; } /** * Returns the corresponding {@link TransactionType} given the accounttype and the effect which the transaction * type should have on the account balance * @param accountType Type of account * @param shouldReduceBalance <code>true</code> if type should reduce balance, <code>false</code> otherwise * @return TransactionType for the account */ public static TransactionType getTypeForBalance(AccountType accountType, boolean shouldReduceBalance){ TransactionType type; if (accountType.hasDebitNormalBalance()) { type = shouldReduceBalance ? TransactionType.CREDIT : TransactionType.DEBIT; } else { type = shouldReduceBalance ? TransactionType.DEBIT : TransactionType.CREDIT; } return type; } /** * Returns true if the transaction type represents a decrease for the account balance for the <code>accountType</code>, false otherwise * @return true if the amount represents a decrease in the account balance, false otherwise * @see #getTypeForBalance(AccountType, boolean) */ public static boolean shouldDecreaseBalance(AccountType accountType, TransactionType transactionType) { if (accountType.hasDebitNormalBalance()) { return transactionType == TransactionType.CREDIT; } else return transactionType == TransactionType.DEBIT; } /** * Sets the exported flag on the transaction * @param isExported <code>true</code> if the transaction has been exported, <code>false</code> otherwise */ public void setExported(boolean isExported){ mIsExported = isExported; } /** * Returns <code>true</code> if the transaction has been exported, <code>false</code> otherwise * @return <code>true</code> if the transaction has been exported, <code>false</code> otherwise */ public boolean isExported(){ return mIsExported; } /** * Returns {@code true} if this transaction is a template, {@code false} otherwise * @return {@code true} if this transaction is a template, {@code false} otherwise */ public boolean isTemplate(){ return mIsTemplate; } /** * Sets flag indicating whether this transaction is a template or not * @param isTemplate Flag indicating if transaction is a template or not */ public void setTemplate(boolean isTemplate){ mIsTemplate = isTemplate; } /** * Converts transaction to XML DOM corresponding to OFX Statement transaction and * returns the element node for the transaction. * The Unique ID of the account is needed in order to properly export double entry transactions * @param doc XML document to which transaction should be added * @param accountUID Unique Identifier of the account which called the method. @return Element in DOM corresponding to transaction */ public Element toOFX(Document doc, String accountUID){ Money balance = getBalance(accountUID); TransactionType transactionType = balance.isNegative() ? TransactionType.DEBIT : TransactionType.CREDIT; Element transactionNode = doc.createElement(OfxHelper.TAG_STATEMENT_TRANSACTION); Element typeNode = doc.createElement(OfxHelper.TAG_TRANSACTION_TYPE); typeNode.appendChild(doc.createTextNode(transactionType.toString())); transactionNode.appendChild(typeNode); Element datePosted = doc.createElement(OfxHelper.TAG_DATE_POSTED); datePosted.appendChild(doc.createTextNode(OfxHelper.getOfxFormattedTime(mTimestamp))); transactionNode.appendChild(datePosted); Element dateUser = doc.createElement(OfxHelper.TAG_DATE_USER); dateUser.appendChild(doc.createTextNode( OfxHelper.getOfxFormattedTime(mTimestamp))); transactionNode.appendChild(dateUser); Element amount = doc.createElement(OfxHelper.TAG_TRANSACTION_AMOUNT); amount.appendChild(doc.createTextNode(balance.toPlainString())); transactionNode.appendChild(amount); Element transID = doc.createElement(OfxHelper.TAG_TRANSACTION_FITID); transID.appendChild(doc.createTextNode(getUID())); transactionNode.appendChild(transID); Element name = doc.createElement(OfxHelper.TAG_NAME); name.appendChild(doc.createTextNode(mDescription)); transactionNode.appendChild(name); if (mNotes != null && mNotes.length() > 0){ Element memo = doc.createElement(OfxHelper.TAG_MEMO); memo.appendChild(doc.createTextNode(mNotes)); transactionNode.appendChild(memo); } if (mSplitList.size() == 2){ //if we have exactly one other split, then treat it like a transfer String transferAccountUID = accountUID; for (Split split : mSplitList) { if (!split.getAccountUID().equals(accountUID)){ transferAccountUID = split.getAccountUID(); break; } } Element bankId = doc.createElement(OfxHelper.TAG_BANK_ID); bankId.appendChild(doc.createTextNode(OfxHelper.APP_ID)); Element acctId = doc.createElement(OfxHelper.TAG_ACCOUNT_ID); acctId.appendChild(doc.createTextNode(transferAccountUID)); Element accttype = doc.createElement(OfxHelper.TAG_ACCOUNT_TYPE); AccountsDbAdapter acctDbAdapter = AccountsDbAdapter.getInstance(); OfxAccountType ofxAccountType = Account.convertToOfxAccountType(acctDbAdapter.getAccountType(transferAccountUID)); accttype.appendChild(doc.createTextNode(ofxAccountType.toString())); Element bankAccountTo = doc.createElement(OfxHelper.TAG_BANK_ACCOUNT_TO); bankAccountTo.appendChild(bankId); bankAccountTo.appendChild(acctId); bankAccountTo.appendChild(accttype); transactionNode.appendChild(bankAccountTo); } return transactionNode; } /** * Returns the GUID of the {@link org.gnucash.android.model.ScheduledAction} which created this transaction * @return GUID of scheduled action */ public String getScheduledActionUID() { return mScheduledActionUID; } /** * Sets the GUID of the {@link org.gnucash.android.model.ScheduledAction} which created this transaction * @param scheduledActionUID GUID of the scheduled action */ public void setScheduledActionUID(String scheduledActionUID) { mScheduledActionUID = scheduledActionUID; } /** * Creates an Intent with arguments from the <code>transaction</code>. * This intent can be broadcast to create a new transaction * @param transaction Transaction used to create intent * @return Intent with transaction details as extras */ public static Intent createIntent(Transaction transaction){ Intent intent = new Intent(Intent.ACTION_INSERT); intent.setType(Transaction.MIME_TYPE); intent.putExtra(Intent.EXTRA_TITLE, transaction.getDescription()); intent.putExtra(Intent.EXTRA_TEXT, transaction.getNote()); intent.putExtra(Account.EXTRA_CURRENCY_CODE, transaction.getCurrencyCode()); StringBuilder stringBuilder = new StringBuilder(); for (Split split : transaction.getSplits()) { stringBuilder.append(split.toCsv()).append("\n"); } intent.putExtra(Transaction.EXTRA_SPLITS, stringBuilder.toString()); return intent; } }