biz.wolschon.finance.jgnucash.widgets.TransactionSum.java Source code

Java tutorial

Introduction

Here is the source code for biz.wolschon.finance.jgnucash.widgets.TransactionSum.java

Source

/**
 * TransactionSum.java
 * created: 09.12.2007 12:06:22
 * (c) 2007 by
 * <a href="http://Wolschon.biz">Wolschon Softwaredesign und Beratung</a>
 * This file is part of jgnucashLib-V1 by Marcus Wolschon
 * <a href="mailto:Marcus@Wolscon.biz">Marcus@Wolscon.biz</a>.
 * You can purchase support for a sensible hourly rate or
 * a commercial license of this file (unless modified by others)
 * by contacting him directly.
 *
 *  jgnucashLib-V1 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  jgnucashLib-V1 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with jgnucashLib-V1.  If not, see <http://www.gnu.org/licenses/>.
 *
 ***********************************
 * Editing this file:
 *  -For consistent code-quality this file should be checked with the
 *   checkstyle-ruleset enclosed in this project.
 *  -After the design of this file has settled it should get it's own
 *   JUnit-Test that shall be executed regularly. It is best to write
 *   the test-case BEFORE writing this class and to run it on every build
 *   as a regression-test.
 */
package biz.wolschon.finance.jgnucash.widgets;

//automatically created propertyChangeListener-Support
import java.awt.BorderLayout;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.xml.bind.JAXBException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import biz.wolschon.fileformats.gnucash.GnucashAccount;
import biz.wolschon.fileformats.gnucash.GnucashFile;
import biz.wolschon.fileformats.gnucash.GnucashTransaction;
import biz.wolschon.fileformats.gnucash.GnucashTransactionSplit;
import biz.wolschon.finance.ComplexCurrencyTable;
import biz.wolschon.numbers.FixedPointNumber;

/**
 * (c) 2007 by <a href="http://Wolschon.biz>
 * Wolschon Softwaredesign und Beratung</a>.<br/>
 * Project: jgnucashLib-V1<br/>
 * TransactionSum.java<br/>
 * created: 09.12.2007 12:06:22 <br/>
 *<br/><br/>
 * This panel displays a sum of all transaction-splits that are
 * to any of a list of accounts belonging to transactions with at
 * least one split in another list of accounts.<br/>
 * It is very handy for tax- and other reports.
 * @author <a href="mailto:Marcus@Wolschon.biz">Marcus Wolschon</a>
 */
public class TransactionSum extends JPanel {

    /**
     * For serializing.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Automatically created logger for debug and error-output.
     */
    /**
     * Our logger for debug- and error-ourput.
     */
    private static final Log LOGGER = LogFactory.getLog(TransactionSum.class);

    /**
     * The financial data we operate on.
     */
    private GnucashFile myBooks;

    /**
     * We only operate on transactions that
     * contain one of these accounts.
     */
    private Set<GnucashAccount> mySourceAccounts;

    /**
     * We sum all transaction-splits that are to
     * one of these accounts.
     * @see #mySourceAccounts
     */
    private Set<GnucashAccount> myTargetAccounts;

    /**
     * We ignore all transactions that are before this date.
     */
    private Date myMinDate;
    /**
     * We ignore all transactions that are after this date.
     */
    private Date myMaxDate;

    /**
     * The type of summations we are to calculate.
     */
    private SUMMATIONTYPE mySummationType = SUMMATIONTYPE.ALL;

    /**
     * (c) 2007 by <a href="http://Wolschon.biz>
     * Wolschon Softwaredesign und Beratung</a>.<br/>
     * Project: jgnucashLib-V1<br/>
     * TransactionSum.java<br/>
     * created: 09.12.2007 12:34:53 <br/>
     *<br/><br/>
     * The types of summations we can do.
     * @author <a href="mailto:Marcus@Wolschon.biz">Marcus Wolschon</a>
     */
    public enum SUMMATIONTYPE {
        /**
         * Sum all splits.
         */
        ALL,
        /**
         * Sum only splits that increase the balance on
         * the targetAccount.
         * @see TransactionSum#myTargetAccounts
         */
        ONLYTO,
        /**
         * Sum only splits that decrease the balance on
         * the targetAccount.
         * @see TransactionSum#myTargetAccounts
         */
        ONLYFROM;

        /**
         * @param aProperty parse this string
         * @return and return the value that matches the name
         */
        public static SUMMATIONTYPE getByName(final String aProperty) {
            if (aProperty.equalsIgnoreCase("all")) {
                return ALL;
            }

            if (aProperty.equalsIgnoreCase("to")) {
                return ONLYTO;
            }
            if (aProperty.equalsIgnoreCase("onlyto")) {
                return ONLYTO;
            }

            if (aProperty.equalsIgnoreCase("from")) {
                return ONLYFROM;
            }
            if (aProperty.equalsIgnoreCase("onlyfrom")) {
                return ONLYFROM;
            }
            if (aProperty.equalsIgnoreCase("allrecursive")) {
                return ALL;
            }

            return ALL;
        }
    }

    /**
     * The label that contains the sum.
     */
    private final JLabel mySumLabel = new JLabel();
    /**
     * The label that contains the name
     * to display left of the sum.
     */
    private final JLabel myNameLabel = new JLabel();
    /**
     * The label to display a link
     * for a drilldown to the list of
     * transactions covered.
     */
    private final JLabel myDrilldownLabel = new JLabel();

    /**
     * The latest value calculated by {@link #reCalculate()}.
     */
    private FixedPointNumber myValue = null;

    /**
     * The count of transactions we counted n the last {@link #reCalculate()}.
     */
    private int myTransactionsCounted = -1;

    /**
     * @param books The financial data we operate on.
     * @param summationType The type of summations we are to calculate.
     * @param targetAccounts We sum all transaction-splits that are to
     *                       one of these accounts.
     * @param sourceAccounts We only operate on transactions that
     *                       contain one of these accounts.
     * @param name the name to display left of the sum
     * @param minDate We ignore all transactions that are before this date.
     * @param maxDate We ignore all transactions that are after this date.
     * @throws JAXBException if we have issues with the XML-backend
     */
    public TransactionSum(final GnucashFile books, final Set<GnucashAccount> sourceAccounts,
            final Set<GnucashAccount> targetAccounts, final SUMMATIONTYPE summationType, final String name,
            final Date minDate, final Date maxDate) throws JAXBException {
        initializeUI(name);
        setName(name);
        setBooks(books);
        setSummationType(summationType);
        setSourceAccounts(sourceAccounts);
        setTargetAccounts(targetAccounts);
        setMinDate(minDate);
        setMaxDate(maxDate);
    }

    /**
     * Do the actual calculation.
     * @throws JAXBException if we have issues with the XML-backend
     */
    private void reCalculate() throws JAXBException {
        if (getSummationType() == null || getSourceAccounts() == null || getTargetAccounts() == null
                || getSummationType() == null
                //|| getMinDate() == null
                //|| getMaxDate() == null
                || getBooks() == null) {
            mySumLabel.setText("---");
            return;
        }
        myTransactionsCounted = 0;

        Set<GnucashAccount> sourceAccounts = new HashSet<GnucashAccount>(getSourceAccounts());
        Set<GnucashAccount> targetAccounts = new HashSet<GnucashAccount>(
                buildTransitiveClosure(getTargetAccounts()));
        Set<String> targetAccountsIDs = new HashSet<String>();
        for (GnucashAccount targetAccount : targetAccounts) {
            targetAccountsIDs.add(targetAccount.getId());
        }

        ////////////////////////////////////
        // find all applicable transacion
        Set<GnucashTransactionSplit> transactions = new HashSet<GnucashTransactionSplit>();
        FixedPointNumber sum = new FixedPointNumber(0);
        if (sourceAccounts.size() == 0) {
            LOGGER.warn("There are no source-accounts given for this transaction-sum");
        }
        for (GnucashAccount sourceAccount : sourceAccounts) {
            FixedPointNumber addMe = buildSum(sourceAccount, targetAccountsIDs,
                    sourceAccount.getCurrencyNameSpace(), sourceAccount.getCurrencyID(), transactions);
            if (addMe == null) {
                mySumLabel.setText("   cannot determine sum");
                sum = null;
                break;
            }
            sum = sum.add(addMe);
        }

        setValue(sum);
        ////////////////////////////////////
        // set output
        Iterator<GnucashAccount> iterator = targetAccounts.iterator();
        if (iterator.hasNext()) {
            mySumLabel.setText("   " + sum.toString() + "" + iterator.next().getCurrencyID());
        } else {
            Iterator<GnucashAccount> iterator2 = sourceAccounts.iterator();
            if (iterator2.hasNext()) {
                mySumLabel.setText("   " + sum.toString() + "" + iterator2.next().getCurrencyID());
            } else {
                mySumLabel.setText("   no account");
            }
        }
    }

    /**
     * @param aSourceAccount
     * @param aTargetAccountsIDs
     * @param alreadyHandled all transactions we have already visited (if multiple target-accounts are involved)
     * @return
     * @throws JAXBException
     */
    private FixedPointNumber buildSum(final GnucashAccount aSourceAccount, final Set<String> aTargetAccountsIDs,
            final String currencyNameSpace, final String currencyID,
            final Set<GnucashTransactionSplit> alreadyHandled) throws JAXBException {

        FixedPointNumber sum = new FixedPointNumber();
        for (Object element : aSourceAccount.getChildren()) {
            GnucashAccount child = (GnucashAccount) element;
            sum = sum.add(buildSum(child, aTargetAccountsIDs, currencyNameSpace, currencyID, alreadyHandled));
        }

        List<? extends GnucashTransactionSplit> splits = aSourceAccount.getTransactionSplits();
        for (GnucashTransactionSplit split : splits) {
            GnucashTransaction transaction = split.getTransaction();
            if (getMinDate() != null && transaction.getDatePosted().before(getMinDate())) {
                continue;
            }
            if (getMaxDate() != null && transaction.getDatePosted().after(getMaxDate())) {
                continue;
            }
            if (aTargetAccountsIDs.size() > 0 && !hasSplitWithAccount(transaction, aTargetAccountsIDs)) {
                continue;
            }
            if (alreadyHandled.contains(split)) {
                continue;
            }
            alreadyHandled.add(split);

            if (getSummationType().equals(SUMMATIONTYPE.ONLYFROM) && split.getQuantity().isPositive()) {
                continue;
            } else if (getSummationType().equals(SUMMATIONTYPE.ONLYTO) && !split.getQuantity().isPositive()) {
                continue;
            }
            if (aSourceAccount.getCurrencyNameSpace().equals(currencyNameSpace)
                    && aSourceAccount.getCurrencyID().equals(currencyID)) {

                sum = sum.add(split.getQuantity());
            } else {
                FixedPointNumber addMe = new FixedPointNumber(split.getQuantity());
                // do not convert 0
                if (!addMe.equals(new FixedPointNumber())) {
                    addMe = convert(aSourceAccount.getCurrencyNameSpace(), aSourceAccount.getCurrencyID(), addMe,
                            currencyNameSpace, currencyID);
                }
                if (addMe == null) {
                    return null;
                }
                sum = sum.add(addMe);
            }
            myTransactionsCounted++;

        }
        return sum;
    }

    /**
     * @param aTransaction
     * @param aTargetAccountsIDs
     * @return
     * @throws JAXBException
     */
    private boolean hasSplitWithAccount(GnucashTransaction aTransaction, Set<String> aTargetAccountsIDs)
            throws JAXBException {
        List<? extends GnucashTransactionSplit> splits = aTransaction.getSplits();
        for (GnucashTransactionSplit split : splits) {
            if (aTargetAccountsIDs.contains(split.getAccountID())) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param aCurrencyNameSpace
     * @param aCurrencyID
     * @param aSum
     * @param aCurrencyNameSpace2
     * @param aCurrencyName
     * @return
     */
    private FixedPointNumber convert(final String aCurrencyNameSpaceFrom, final String aCurrencyIDFrom,
            final FixedPointNumber aSum, final String aCurrencyNameSpaceTo, final String aCurrencyIDTo) {
        ComplexCurrencyTable currencyTable = getBooks().getCurrencyTable();

        if (currencyTable == null) {
            LOGGER.warn("SimpleAccount.getBalance() - cannot transfer "
                    + "to given currency because we have no currency-table!");
            return null;
        }
        FixedPointNumber sum = new FixedPointNumber(aSum);

        if (!currencyTable.convertToBaseCurrency(aCurrencyNameSpaceFrom, sum, aCurrencyIDFrom)) {
            Collection<String> currencies = getBooks().getCurrencyTable().getCurrencies(aCurrencyNameSpaceFrom);
            LOGGER.warn("SimpleAccount.getBalance() - cannot transfer " + "from our currency '"
                    + aCurrencyNameSpaceFrom + "'-'" + aCurrencyIDFrom + "' to the base-currency!" + " \n(we know "
                    + getBooks().getCurrencyTable().getNameSpaces().size() + " currency-namespaces and "
                    + (currencies == null ? "no" : "" + currencies.size()) + " currencies in our namespace)");
            return null;
        }

        if (!currencyTable.convertFromBaseCurrency(aCurrencyNameSpaceTo, sum, aCurrencyIDTo)) {
            LOGGER.warn("SimpleAccount.getBalance() - cannot transfer " + "from base-currenty to given currency '"
                    + aCurrencyNameSpaceTo + "-" + aCurrencyIDTo + "'!");
            return null;
        }
        return sum;
    }

    /**
     * Build the transitive closure of a list of accounts
     * by adding all child-accounts.
     * @param accounts the account-list to walk
     * @return a set of all given accounts and all their child-accounts.
     */
    private Collection<? extends GnucashAccount> buildTransitiveClosure(
            final Collection<? extends GnucashAccount> accounts) {

        if (accounts.size() == 0) {
            return accounts;
        }

        Set<GnucashAccount> retval = new HashSet<GnucashAccount>(accounts);

        // TODO implement TransactionSum.buildTransitiveClosure
        for (GnucashAccount account : accounts) {
            Collection<? extends GnucashAccount> allChildren = buildTransitiveClosure(account.getChildren());
            retval.addAll(allChildren);
        }
        return retval;
    }

    /**
     * Create the UI-components.
     * @param name the name to display left of the sum
     */
    public void initializeUI(final String name) {
        this.setLayout(new BorderLayout());
        myNameLabel.setText(name);
        mySumLabel.setText("...");
        myDrilldownLabel.setText("");//TODO: implement drilldown
        this.add(myNameLabel, BorderLayout.WEST);
        this.add(mySumLabel, BorderLayout.CENTER);
        this.add(myDrilldownLabel, BorderLayout.EAST);
    }
    //------------------------ support for propertyChangeListeners -------------

    /**
     * support for firing PropertyChangeEvents.
     * (gets initialized only if we really have listeners)
     */
    private volatile PropertyChangeSupport myPropertyChange = null;

    /**
     * Returned value may be null if we never had listeners.
     * @return Our support for firing PropertyChangeEvents
     */
    protected final PropertyChangeSupport getPropertyChangeSupport() {
        return myPropertyChange;
    }

    /**
     * Add a PropertyChangeListener to the listener list.
     * The listener is registered for all properties.
     *
     * @param listener  The PropertyChangeListener to be added
     */
    @Override
    public final void addPropertyChangeListener(final PropertyChangeListener listener) {
        if (myPropertyChange == null) {
            myPropertyChange = new PropertyChangeSupport(this);
        }
        myPropertyChange.addPropertyChangeListener(listener);
    }

    /**
     * Add a PropertyChangeListener for a specific property.  The listener
     * will be invoked only when a call on firePropertyChange names that
     * specific property.
     *
     * @param propertyName  The name of the property to listen on.
     * @param listener  The PropertyChangeListener to be added
     */
    @Override
    public final void addPropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
        if (myPropertyChange == null) {
            myPropertyChange = new PropertyChangeSupport(this);
        }
        myPropertyChange.addPropertyChangeListener(propertyName, listener);
    }

    /**
     * Remove a PropertyChangeListener for a specific property.
     *
     * @param propertyName  The name of the property that was listened on.
     * @param listener  The PropertyChangeListener to be removed
     */
    @Override
    public final void removePropertyChangeListener(final String propertyName,
            final PropertyChangeListener listener) {
        if (myPropertyChange != null) {
            myPropertyChange.removePropertyChangeListener(propertyName, listener);
        }
    }

    /**
     * Remove a PropertyChangeListener from the listener list.
     * This removes a PropertyChangeListener that was registered
     * for all properties.
     *
     * @param listener  The PropertyChangeListener to be removed
     */
    @Override
    public final synchronized void removePropertyChangeListener(final PropertyChangeListener listener) {
        if (myPropertyChange != null) {
            myPropertyChange.removePropertyChangeListener(listener);
        }
    }

    //-------------------------------------------------------

    /**
     * @return Returns the books.
     * @see #myBooks
     */
    public GnucashFile getBooks() {
        return myBooks;
    }

    /**
     * @param aBooks The books to set.
     * @throws JAXBException if we have issues with the XML-backend
     * @see #myBooks
     */
    public void setBooks(final GnucashFile aBooks) throws JAXBException {
        if (aBooks == null) {
            throw new IllegalArgumentException("null 'aBooks' given!");
        }

        Object old = myBooks;
        if (old == aBooks) {
            return; // nothing has changed
        }
        myBooks = aBooks;
        // <<insert code to react further to this change here
        reCalculate();
        PropertyChangeSupport propertyChangeFirer = getPropertyChangeSupport();
        if (propertyChangeFirer != null) {
            propertyChangeFirer.firePropertyChange("aBooks", old, aBooks);
        }
    }

    /**
     * @return Returns the sourceAccounts.
     * @see #mySourceAccounts
     */
    public Set<GnucashAccount> getSourceAccounts() {
        return mySourceAccounts;
    }

    /**
     * @param aSourceAccounts The sourceAccounts to set.
     * @throws JAXBException if we have issues with the XML-backend
     * @see #mySourceAccounts
     */
    public void setSourceAccounts(final Set<GnucashAccount> aSourceAccounts) throws JAXBException {
        if (aSourceAccounts == null) {
            throw new IllegalArgumentException("null 'aSourceAccounts' given!");
        }

        Object old = mySourceAccounts;
        if (old == aSourceAccounts) {
            return; // nothing has changed
        }
        mySourceAccounts = aSourceAccounts;
        // <<insert code to react further to this change here
        reCalculate();
        PropertyChangeSupport propertyChangeFirer = getPropertyChangeSupport();
        if (propertyChangeFirer != null) {
            propertyChangeFirer.firePropertyChange("aSourceAccounts", old, aSourceAccounts);
        }
    }

    /**
     * @return Returns the summationType.
     * @see #mySummationType
     */
    public SUMMATIONTYPE getSummationType() {
        return mySummationType;
    }

    /**
     * @param aSummationType The summationType to set.
     * @throws JAXBException if we have issues with the XML-backend
     * @see #mySummationType
     */
    public void setSummationType(final SUMMATIONTYPE aSummationType) throws JAXBException {
        if (aSummationType == null) {
            throw new IllegalArgumentException("null 'aSummationType' given!");
        }

        Object old = mySummationType;
        if (old == aSummationType) {
            return; // nothing has changed
        }
        mySummationType = aSummationType;
        // <<insert code to react further to this change here
        reCalculate();
        PropertyChangeSupport propertyChangeFirer = getPropertyChangeSupport();
        if (propertyChangeFirer != null) {
            propertyChangeFirer.firePropertyChange("aSummationType", old, aSummationType);
        }
    }

    /**
     * @return Returns the targetAccounts.
     * @see #myTargetAccounts
     */
    public Set<GnucashAccount> getTargetAccounts() {
        return myTargetAccounts;
    }

    /**
     * @param aTargetAccounts The targetAccounts to set.
     * @throws JAXBException if we have issues with the XML-backend
     * @see #myTargetAccounts
     */
    public void setTargetAccounts(final Set<GnucashAccount> aTargetAccounts) throws JAXBException {
        if (aTargetAccounts == null) {
            throw new IllegalArgumentException("null 'aTargetAccounts' given!");
        }

        Object old = myTargetAccounts;
        if (old == aTargetAccounts) {
            return; // nothing has changed
        }
        myTargetAccounts = aTargetAccounts;
        // <<insert code to react further to this change here
        reCalculate();
        PropertyChangeSupport propertyChangeFirer = getPropertyChangeSupport();
        if (propertyChangeFirer != null) {
            propertyChangeFirer.firePropertyChange("aTargetAccounts", old, aTargetAccounts);
        }
    }

    /**
     * @return Returns the minDate.
     * @see #myMinDate
     */
    public Date getMinDate() {
        return myMinDate;
    }

    /**
     * @param aMinDate The minDate to set.
     * @throws JAXBException if we have issues with the XML-backend
     * @see #myMinDate
     */
    public void setMinDate(final Date aMinDate) throws JAXBException {
        //        if (aMinDate == null) {
        //            throw new IllegalArgumentException("null 'aMinDate' given!");
        //        }

        Object old = myMinDate;
        if (old == aMinDate) {
            return; // nothing has changed
        }
        myMinDate = aMinDate;
        // <<insert code to react further to this change here
        reCalculate();
        PropertyChangeSupport propertyChangeFirer = getPropertyChangeSupport();
        if (propertyChangeFirer != null) {
            propertyChangeFirer.firePropertyChange("aMinDate", old, aMinDate);
        }
    }

    /**
     * @return Returns the maxDate.
     * @see #myMaxDate
     */
    public Date getMaxDate() {
        return myMaxDate;
    }

    /**
     * @param aMaxDate The maxDate to set.
     * @throws JAXBException if we have issues with the XML-backend
     * @see #myMaxDate
     */
    public void setMaxDate(final Date aMaxDate) throws JAXBException {
        //        if (aMaxDate == null) {
        //            throw new IllegalArgumentException("null 'aMaxDate' given!");
        //        }

        Object old = myMaxDate;
        if (old == aMaxDate) {
            return; // nothing has changed
        }
        myMaxDate = aMaxDate;
        // <<insert code to react further to this change here
        reCalculate();
        PropertyChangeSupport propertyChangeFirer = getPropertyChangeSupport();
        if (propertyChangeFirer != null) {
            propertyChangeFirer.firePropertyChange("aMaxDate", old, aMaxDate);
        }
    }

    /**
     * @return the value
     */
    public FixedPointNumber getValue() {
        return myValue;
    }

    /**
     * @param aValue the value to set
     */
    private void setValue(final FixedPointNumber aValue) {
        myValue = aValue;
    }

    /**
     * @return the transactionsCounted
     */
    public int getTransactionsCounted() {
        return myTransactionsCounted;
    }
}