package org.gnucash.android.model; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import org.gnucash.android.db.adapter.AccountsDbAdapter; import java.sql.Timestamp; /** * A split amount in a transaction. * Every transaction is made up of at least two splits (representing a double entry transaction) * <p>The split amount is always stored in the database as the absolute value alongside its transaction type of CREDIT/DEBIT<br/> * This is independent of the negative values which are shown in the UI (for user convenience). * The actual movement of the balance in the account depends on the type of normal balance of the account and the * transaction type of the split.</p> * * @author Ngewi Fet <ngewif@gmail.com> */ public class Split extends BaseModel implements Parcelable{ /** * Flag indicating that the split has been reconciled */ public static final char FLAG_RECONCILED = 'y'; /** * Flag indicating that the split has not been reconciled */ public static final char FLAG_NOT_RECONCILED = 'n'; /** * Flag indicating that the split has been cleared, but not reconciled */ public static final char FLAG_CLEARED = 'c'; /** * Amount value of this split which is in the currency of the transaction */ private Money mValue; /** * Amount of the split in the currency of the account to which the split belongs */ private Money mQuantity; /** * Transaction UID which this split belongs to */ private String mTransactionUID = ""; /** * Account UID which this split belongs to */ private String mAccountUID; /** * The type of this transaction, credit or debit */ private TransactionType mSplitType = TransactionType.CREDIT; /** * Memo associated with this split */ private String mMemo; private char mReconcileState = FLAG_NOT_RECONCILED; /** * Database required non-null field */ private Timestamp mReconcileDate = new Timestamp(System.currentTimeMillis()); /** * Initialize split with a value amount and account * @param value Money value amount of this split * @param accountUID String UID of transfer account */ public Split(@NonNull Money value, @NonNull Money quantity, String accountUID){ setQuantity(quantity); setValue(value); setAccountUID(accountUID); //NOTE: This is a rather simplististic approach to the split type. //It typically also depends on the account type of the account. But we do not want to access //the database everytime a split is created. So we keep it simple here. Set the type you want explicity. mSplitType = value.isNegative() ? TransactionType.DEBIT : TransactionType.CREDIT; } /** * Initialize split with a value amount and account * @param amount Money value amount of this split. Value is always in the currency the owning transaction. * This amount will be assigned as both the value and the quantity of this split * @param accountUID String UID of owning account */ public Split(@NonNull Money amount, String accountUID){ setValue(amount); setQuantity(new Money(amount)); setAccountUID(accountUID); //NOTE: This is a rather simplististic approach to the split type. //It typically also depends on the account type of the account. But we do not want to access //the database everytime a split is created. So we keep it simple here. Set the type you want explicity. mSplitType = amount.isNegative() ? TransactionType.DEBIT : TransactionType.CREDIT; } /** * Clones the <code>sourceSplit</code> to create a new instance with same fields * @param sourceSplit Split to be cloned * @param generateUID Determines if the clone should have a new UID or should maintain the one from source */ public Split(Split sourceSplit, boolean generateUID){ this.mMemo = sourceSplit.mMemo; this.mAccountUID = sourceSplit.mAccountUID; this.mSplitType = sourceSplit.mSplitType; this.mTransactionUID = sourceSplit.mTransactionUID; this.mValue = new Money(sourceSplit.mValue); this.mQuantity = new Money(sourceSplit.mQuantity); //todo: clone reconciled status if (generateUID){ generateUID(); } else { setUID(sourceSplit.getUID()); } } /** * Returns the value amount of the split * @return Money amount of the split with the currency of the transaction * @see #getQuantity() */ public Money getValue() { return mValue; } /** * Sets the value amount of the split.<br> * The value is in the currency of the containing transaction * @param value Money value of this split * @see #setQuantity(Money) */ public void setValue(Money value) { mValue = value; } /** * Returns the quantity amount of the split. * <p>The quantity is in the currency of the account to which the split is associated</p> * @return Money quantity amount * @see #getValue() */ public Money getQuantity() { return mQuantity; } /** * Sets the quantity value of the split * @param quantity Money quantity amount * @see #setValue(Money) */ public void setQuantity(Money quantity) { this.mQuantity = quantity; } /** * Returns transaction GUID to which the split belongs * @return String GUID of the transaction */ public String getTransactionUID() { return mTransactionUID; } /** * Sets the transaction to which the split belongs * @param transactionUID GUID of transaction */ public void setTransactionUID(String transactionUID) { this.mTransactionUID = transactionUID; } /** * Returns the account GUID of this split * @return GUID of the account */ public String getAccountUID() { return mAccountUID; } /** * Sets the GUID of the account of this split * @param accountUID GUID of account */ public void setAccountUID(String accountUID) { this.mAccountUID = accountUID; } /** * Returns the type of the split * @return {@link TransactionType} of the split */ public TransactionType getType() { return mSplitType; } /** * Sets the type of this split * @param splitType Type of the split */ public void setType(TransactionType splitType) { this.mSplitType = splitType; } /** * Returns the memo of this split * @return String memo of this split */ public String getMemo() { return mMemo; } /** * Sets this split memo * @param memo String memo of this split */ public void setMemo(String memo) { this.mMemo = memo; } /** * Creates a split which is a pair of this instance. * A pair split has all the same attributes except that the SplitType is inverted and it belongs * to another account. * @param accountUID GUID of account * @return New split pair of current split * @see TransactionType#invert() */ public Split createPair(String accountUID){ Split pair = new Split(mValue.abs(), accountUID); pair.setType(mSplitType.invert()); pair.setMemo(mMemo); pair.setTransactionUID(mTransactionUID); pair.setQuantity(mQuantity); return pair; } /** * Clones this split and returns an exact copy. * @return New instance of a split which is a copy of the current one */ protected Split clone() throws CloneNotSupportedException { super.clone(); Split split = new Split(mValue, mAccountUID); split.setUID(getUID()); split.setType(mSplitType); split.setMemo(mMemo); split.setTransactionUID(mTransactionUID); split.setQuantity(mQuantity); return split; } /** * Checks is this <code>other</code> is a pair split of this. * <p>Two splits are considered a pair if they have the same amount and opposite split types</p> * @param other the other split of the pair to be tested * @return whether the two splits are a pair */ public boolean isPairOf(Split other) { return mValue.abs().equals(other.mValue.abs()) && mSplitType.invert().equals(other.mSplitType); } /** * Returns the formatted amount (with or without negation sign) for the split value * @return Money amount of value * @see #getFormattedAmount(Money, String, TransactionType) */ public Money getFormattedValue(){ return getFormattedAmount(mValue, mAccountUID, mSplitType); } /** * Returns the formatted amount (with or without negation sign) for the quantity * @return Money amount of quantity * @see #getFormattedAmount(Money, String, TransactionType) */ public Money getFormattedQuantity(){ return getFormattedAmount(mQuantity, mAccountUID, mSplitType); } /** * Splits are saved as absolute values to the database, with no negative numbers. * The type of movement the split causes to the balance of an account determines its sign, and * that depends on the split type and the account type * @param amount Money amount to format * @param accountUID GUID of the account * @param splitType Transaction type of the split * @return -{@code amount} if the amount would reduce the balance of {@code account}, otherwise +{@code amount} */ public static Money getFormattedAmount(Money amount, String accountUID, TransactionType splitType){ boolean isDebitAccount = AccountsDbAdapter.getInstance().getAccountType(accountUID).hasDebitNormalBalance(); Money absAmount = amount.abs(); boolean isDebitSplit = splitType == TransactionType.DEBIT; if (isDebitAccount) { if (isDebitSplit) { return absAmount; } else { return absAmount.negate(); } } else { if (isDebitSplit) { return absAmount.negate(); } else { return absAmount; } } } /** * Return the reconciled state of this split * <p> * The reconciled state is one of the following values: * <ul> * <li><b>y</b>: means this split has been reconciled</li> * <li><b>n</b>: means this split is not reconciled</li> * <li><b>c</b>: means split has been cleared, but not reconciled</li> * </ul> * </p> <br> * You can check the return value against the reconciled flags {@link #FLAG_RECONCILED}, {@link #FLAG_NOT_RECONCILED}, {@link #FLAG_CLEARED} * @return Character showing reconciled state */ public char getReconcileState() { return mReconcileState; } /** * Check if this split is reconciled * @return {@code true} if the split is reconciled, {@code false} otherwise */ public boolean isReconciled(){ return mReconcileState == FLAG_RECONCILED; } /** * Set reconciled state of this split. * <p> * The reconciled state is one of the following values: * <ul> * <li><b>y</b>: means this split has been reconciled</li> * <li><b>n</b>: means this split is not reconciled</li> * <li><b>c</b>: means split has been cleared, but not reconciled</li> * </ul> * </p> * @param reconcileState One of the following flags {@link #FLAG_RECONCILED}, {@link #FLAG_NOT_RECONCILED}, {@link #FLAG_CLEARED} */ public void setReconcileState(char reconcileState) { this.mReconcileState = reconcileState; } /** * Return the date of reconciliation * @return Timestamp */ public Timestamp getReconcileDate() { return mReconcileDate; } /** * Set reconciliation date for this split * @param reconcileDate Timestamp of reconciliation */ public void setReconcileDate(Timestamp reconcileDate) { this.mReconcileDate = reconcileDate; } @Override public String toString() { return mSplitType.name() + " of " + mValue.toString() + " in account: " + mAccountUID; } /** * Returns a string representation of the split which can be parsed again using {@link org.gnucash.android.model.Split#parseSplit(String)} * <p>The string is formatted as:<br/> * "<uid>;<valueNum>;<valueDenom>;<valueCurrencyCode>;<quantityNum>;<quantityDenom>;<quantityCurrencyCode>;<transaction_uid>;<account_uid>;<type>;<memo>" * </p> * <p><b>Only the memo field is allowed to be null</b></p> * @return the converted CSV string of this split */ public String toCsv(){ String sep = ";"; //TODO: add reconciled state and date String splitString = getUID() + sep + mValue.getNumerator() + sep + mValue.getDenominator() + sep + mValue.getCommodity().getCurrencyCode() + sep + mQuantity.getNumerator() + sep + mQuantity.getDenominator() + sep + mQuantity.getCommodity().getCurrencyCode() + sep + mTransactionUID + sep + mAccountUID + sep + mSplitType.name(); if (mMemo != null){ splitString = splitString + sep + mMemo; } return splitString; } /** * Parses a split which is in the format:<br/> * "<uid>;<valueNum>;<valueDenom>;<currency_code>;<quantityNum>;<quantityDenom>;<currency_code>;<transaction_uid>;<account_uid>;<type>;<memo>". * <p>Also supports parsing of the deprecated format "<amount>;<currency_code>;<transaction_uid>;<account_uid>;<type>;<memo>". * The split input string is the same produced by the {@link Split#toCsv()} method *</p> * @param splitCsvString String containing formatted split * @return Split instance parsed from the string */ public static Split parseSplit(String splitCsvString) { //TODO: parse reconciled state and date String[] tokens = splitCsvString.split(";"); if (tokens.length < 8) { //old format splits Money amount = new Money(tokens[0], tokens[1]); Split split = new Split(amount, tokens[2]); split.setTransactionUID(tokens[3]); split.setType(TransactionType.valueOf(tokens[4])); if (tokens.length == 6) { split.setMemo(tokens[5]); } return split; } else { long valueNum = Long.parseLong(tokens[1]); long valueDenom = Long.parseLong(tokens[2]); String valueCurrencyCode = tokens[3]; long quantityNum = Long.parseLong(tokens[4]); long quantityDenom = Long.parseLong(tokens[5]); String qtyCurrencyCode = tokens[6]; Money value = new Money(valueNum, valueDenom, valueCurrencyCode); Money quantity = new Money(quantityNum, quantityDenom, qtyCurrencyCode); Split split = new Split(value, tokens[8]); split.setUID(tokens[0]); split.setQuantity(quantity); split.setTransactionUID(tokens[7]); split.setType(TransactionType.valueOf(tokens[9])); if (tokens.length == 11) { split.setMemo(tokens[10]); } return split; } } /** * Two splits are considered equivalent if all the fields (excluding GUID and timestamps - created, modified, reconciled) are equal. * Any two splits which are equal are also equivalent, but the reverse is not true * <p>The difference with to {@link #equals(Object)} is that the GUID of the split is not considered. * This is useful in cases where a new split is generated for a transaction with the same properties, * but a new GUID is generated e.g. when editing a transaction and modifying the splits</p> * * @param split Other split for which to test equivalence * @return {@code true} if both splits are equivalent, {@code false} otherwise */ public boolean isEquivalentTo(Split split){ if (this == split) return true; if (super.equals(split)) return true; if (mReconcileState != split.mReconcileState) return false; if (!mValue.equals(split.mValue)) return false; if (!mQuantity.equals(split.mQuantity)) return false; if (!mTransactionUID.equals(split.mTransactionUID)) return false; if (!mAccountUID.equals(split.mAccountUID)) return false; if (mSplitType != split.mSplitType) return false; return mMemo != null ? mMemo.equals(split.mMemo) : split.mMemo == null; } /** * Two splits are considered equal if all their properties excluding timestampes (created, modified, reconciled) are equal. * @param o Other split to compare for equality * @return {@code true} if this split is equal to {@code o}, {@code false} otherwise */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Split split = (Split) o; if (mReconcileState != split.mReconcileState) return false; if (!mValue.equals(split.mValue)) return false; if (!mQuantity.equals(split.mQuantity)) return false; if (!mTransactionUID.equals(split.mTransactionUID)) return false; if (!mAccountUID.equals(split.mAccountUID)) return false; if (mSplitType != split.mSplitType) return false; return mMemo != null ? mMemo.equals(split.mMemo) : split.mMemo == null; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + mValue.hashCode(); result = 31 * result + mQuantity.hashCode(); result = 31 * result + mTransactionUID.hashCode(); result = 31 * result + mAccountUID.hashCode(); result = 31 * result + mSplitType.hashCode(); result = 31 * result + (mMemo != null ? mMemo.hashCode() : 0); result = 31 * result + (int) mReconcileState; return result; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(getUID()); dest.writeString(mAccountUID); dest.writeString(mTransactionUID); dest.writeString(mSplitType.name()); dest.writeLong(mValue.getNumerator()); dest.writeLong(mValue.getDenominator()); dest.writeString(mValue.getCommodity().getCurrencyCode()); dest.writeLong(mQuantity.getNumerator()); dest.writeLong(mQuantity.getDenominator()); dest.writeString(mQuantity.getCommodity().getCurrencyCode()); dest.writeString(mMemo == null ? "" : mMemo); dest.writeString(String.valueOf(mReconcileState)); dest.writeString(mReconcileDate.toString()); } /** * Constructor for creating a Split object from a Parcel * @param source Source parcel containing the split * @see #CREATOR */ private Split(Parcel source){ setUID(source.readString()); mAccountUID = source.readString(); mTransactionUID = source.readString(); mSplitType = TransactionType.valueOf(source.readString()); long valueNum = source.readLong(); long valueDenom = source.readLong(); String valueCurrency = source.readString(); mValue = new Money(valueNum, valueDenom, valueCurrency); long qtyNum = source.readLong(); long qtyDenom = source.readLong(); String qtyCurrency = source.readString(); mQuantity = new Money(qtyNum, qtyDenom, qtyCurrency); String memo = source.readString(); mMemo = memo.isEmpty() ? null : memo; mReconcileState = source.readString().charAt(0); mReconcileDate = Timestamp.valueOf(source.readString()); } /** * Creates new Parcels containing the information in this split during serialization */ public static final Parcelable.Creator<Split> CREATOR = new Parcelable.Creator<Split>() { @Override public Split createFromParcel(Parcel source) { return new Split(source); } @Override public Split[] newArray(int size) { return new Split[size]; } }; }