001: /*
002: *
003: * JMoney - A Personal Finance Manager
004: * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net>
005: *
006: *
007: * This program is free software; you can redistribute it and/or modify
008: * it under the terms of the GNU General Public License as published by
009: * the Free Software Foundation; either version 2 of the License, or
010: * (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
020: *
021: */
022:
023: package net.sf.jmoney.reconciliation.reconcilePage;
024:
025: import java.net.URL;
026: import java.util.ArrayList;
027: import java.util.Collection;
028: import java.util.Vector;
029:
030: import net.sf.jmoney.JMoneyPlugin;
031: import net.sf.jmoney.entrytable.BalanceColumn;
032: import net.sf.jmoney.entrytable.BaseEntryRowControl;
033: import net.sf.jmoney.entrytable.Block;
034: import net.sf.jmoney.entrytable.ButtonCellControl;
035: import net.sf.jmoney.entrytable.CellBlock;
036: import net.sf.jmoney.entrytable.DebitAndCreditColumns;
037: import net.sf.jmoney.entrytable.EntriesTable;
038: import net.sf.jmoney.entrytable.EntryData;
039: import net.sf.jmoney.entrytable.EntryRowControl;
040: import net.sf.jmoney.entrytable.HorizontalBlock;
041: import net.sf.jmoney.entrytable.ICellControl;
042: import net.sf.jmoney.entrytable.IEntriesContent;
043: import net.sf.jmoney.entrytable.IRowProvider;
044: import net.sf.jmoney.entrytable.IndividualBlock;
045: import net.sf.jmoney.entrytable.OtherEntriesButton;
046: import net.sf.jmoney.entrytable.PropertyBlock;
047: import net.sf.jmoney.entrytable.ReusableRowProvider;
048: import net.sf.jmoney.entrytable.RowSelectionTracker;
049: import net.sf.jmoney.entrytable.SingleOtherEntryPropertyBlock;
050: import net.sf.jmoney.entrytable.SplitEntryRowControl;
051: import net.sf.jmoney.isolation.TransactionManager;
052: import net.sf.jmoney.isolation.UncommittedObjectKey;
053: import net.sf.jmoney.model2.Currency;
054: import net.sf.jmoney.model2.Entry;
055: import net.sf.jmoney.model2.EntryInfo;
056: import net.sf.jmoney.model2.Transaction;
057: import net.sf.jmoney.model2.TransactionInfo;
058: import net.sf.jmoney.reconciliation.BankStatement;
059: import net.sf.jmoney.reconciliation.ReconciliationEntryInfo;
060: import net.sf.jmoney.reconciliation.ReconciliationPlugin;
061:
062: import org.eclipse.jface.dialogs.MessageDialog;
063: import org.eclipse.jface.resource.ImageDescriptor;
064: import org.eclipse.jface.util.LocalSelectionTransfer;
065: import org.eclipse.jface.viewers.StructuredSelection;
066: import org.eclipse.swt.SWT;
067: import org.eclipse.swt.dnd.DND;
068: import org.eclipse.swt.dnd.DragSource;
069: import org.eclipse.swt.dnd.DragSourceEvent;
070: import org.eclipse.swt.dnd.DragSourceListener;
071: import org.eclipse.swt.dnd.Transfer;
072: import org.eclipse.swt.events.DisposeEvent;
073: import org.eclipse.swt.events.DisposeListener;
074: import org.eclipse.swt.graphics.Image;
075: import org.eclipse.swt.widgets.Composite;
076: import org.eclipse.swt.widgets.Label;
077: import org.eclipse.ui.forms.SectionPart;
078: import org.eclipse.ui.forms.widgets.FormToolkit;
079: import org.eclipse.ui.forms.widgets.Section;
080:
081: /**
082: * Class implementing the section containing the unreconciled
083: * entries on the account reconciliation page.
084: *
085: * @author Nigel Westbury
086: */
087: public class UnreconciledSection extends SectionPart {
088:
089: ReconcilePage fPage;
090:
091: EntriesTable fUnreconciledEntriesControl;
092:
093: FormToolkit toolkit;
094:
095: IEntriesContent unreconciledTableContents = null;
096:
097: ArrayList<CellBlock<EntryData, EntryRowControl>> cellList;
098:
099: public UnreconciledSection(ReconcilePage page, Composite parent,
100: RowSelectionTracker rowTracker) {
101: super (parent, page.getManagedForm().getToolkit(),
102: Section.TITLE_BAR);
103: getSection().setText("Unreconciled Entries");
104: fPage = page;
105: this .toolkit = page.getManagedForm().getToolkit();
106:
107: unreconciledTableContents = new IEntriesContent() {
108: public Collection<Entry> getEntries() {
109: /* The caller always sorts, so there is no point in us returning
110: * sorted results. It may be at some point we decide it is more
111: * efficient to get the database to sort for us, but that would
112: * only help the first time the results are fetched, it would not
113: * help on a re-sort. It also only helps if the database indexes
114: * on the date.
115: CurrencyAccount account = fPage.getAccount();
116: Collection<Entry> accountEntries =
117: account
118: .getSortedEntries(TransactionInfo.getDateAccessor(), false);
119: */
120: Collection<Entry> accountEntries = fPage.getAccount()
121: .getEntries();
122:
123: Vector<Entry> requiredEntries = new Vector<Entry>();
124: for (Entry entry : accountEntries) {
125: if (entry.getPropertyValue(ReconciliationEntryInfo
126: .getStatementAccessor()) == null) {
127: requiredEntries.add(entry);
128: }
129: }
130:
131: return requiredEntries;
132: }
133:
134: public boolean isEntryInTable(Entry entry) {
135: // This entry is to be shown if the account
136: // matches and no statement is set.
137: BankStatement statement = entry
138: .getPropertyValue(ReconciliationEntryInfo
139: .getStatementAccessor());
140: return fPage.getAccount().equals(entry.getAccount())
141: && statement == null;
142: }
143:
144: public boolean filterEntry(EntryData data) {
145: // No filter here, so entries always match
146: return true;
147: }
148:
149: public long getStartBalance() {
150: // TODO: figure out how we keep this up to date.
151: // The EntriesTree class has no mechanism for refreshing
152: // the opening balance. It should have.
153: return 0;
154: }
155:
156: public Entry createNewEntry(Transaction newTransaction) {
157: Entry entryInTransaction = newTransaction.createEntry();
158: Entry otherEntry = newTransaction.createEntry();
159:
160: setNewEntryProperties(entryInTransaction);
161:
162: // TODO: See if this code has any effect, and
163: // should this be here at all?
164: /*
165: * We set the currency by default to be the currency of the
166: * top-level entry.
167: *
168: * The currency of an entry is not applicable if the entry is an
169: * entry in a currency account or an income and expense account
170: * that is restricted to a single currency.
171: * However, we set it anyway so the value is there if the entry
172: * is set to an account which allows entries in multiple currencies.
173: *
174: * It may be that the currency of the top-level entry is not
175: * known. This is not possible if entries in a currency account
176: * are being listed, but may be possible if this entries list
177: * control is used for more general purposes. In this case, the
178: * currency is not set and so the user must enter it.
179: */
180: if (entryInTransaction.getCommodity() instanceof Currency) {
181: otherEntry
182: .setIncomeExpenseCurrency((Currency) entryInTransaction
183: .getCommodity());
184: }
185:
186: return entryInTransaction;
187: }
188:
189: private void setNewEntryProperties(Entry newEntry) {
190: // It is assumed that the entry is in a data manager that is a direct
191: // child of the data manager that contains the account.
192: TransactionManager tm = (TransactionManager) newEntry
193: .getDataManager();
194: newEntry.setAccount(tm.getCopyInTransaction(fPage
195: .getAccount()));
196: }
197: };
198:
199: // Load the 'reconcile' indicator
200: URL installURL = ReconciliationPlugin.getDefault().getBundle()
201: .getEntry("/icons/reconcile.gif");
202: final Image reconcileImage = ImageDescriptor.createFromURL(
203: installURL).createImage();
204: parent.addDisposeListener(new DisposeListener() {
205: public void widgetDisposed(DisposeEvent e) {
206: reconcileImage.dispose();
207: }
208: });
209:
210: CellBlock<EntryData, EntryRowControl> reconcileButton = new CellBlock<EntryData, EntryRowControl>(
211: 20, 0) {
212: @Override
213: public ICellControl<EntryData> createCellControl(
214: Composite parent, final EntryRowControl rowControl) {
215: ButtonCellControl cellControl = new ButtonCellControl(
216: rowControl, reconcileImage,
217: "Reconcile this Entry to the above Statement") {
218: @Override
219: protected void run(EntryRowControl rowControl) {
220: reconcileEntry(rowControl);
221: }
222: };
223:
224: // Allow entries in the account to be moved from the unreconciled list
225: final DragSource dragSource = new DragSource(
226: cellControl.getControl(), DND.DROP_MOVE);
227:
228: // Provide data using a local reference only (can only drag and drop
229: // within the Java VM)
230: Transfer[] types = new Transfer[] { LocalSelectionTransfer
231: .getTransfer() };
232: dragSource.setTransfer(types);
233:
234: dragSource.addDragListener(new DragSourceListener() {
235: public void dragStart(DragSourceEvent event) {
236: Entry uncommittedEntry = rowControl
237: .getUncommittedEntryData().getEntry();
238: UncommittedObjectKey uncommittedKey = (UncommittedObjectKey) uncommittedEntry
239: .getObjectKey();
240:
241: // Allow a drag in all cases except where this entry is a new uncommitted entry.
242: // TODO: What if there are uncommitted changes????
243: // We should probably commit changes first.
244: // We can't drag these because the merge is done by re-parenting.
245: if (uncommittedKey.getCommittedObjectKey() == null) {
246: event.doit = false;
247: }
248: }
249:
250: public void dragSetData(DragSourceEvent event) {
251: // Provide the data of the requested type.
252: if (LocalSelectionTransfer.getTransfer()
253: .isSupportedType(event.dataType)) {
254: Entry uncommittedEntry = rowControl
255: .getUncommittedEntryData()
256: .getEntry();
257: UncommittedObjectKey uncommittedKey = (UncommittedObjectKey) uncommittedEntry
258: .getObjectKey();
259: Object sourceEntry = uncommittedKey
260: .getCommittedObjectKey()
261: .getObject();
262: LocalSelectionTransfer.getTransfer()
263: .setSelection(
264: new StructuredSelection(
265: sourceEntry));
266: }
267: }
268:
269: public void dragFinished(DragSourceEvent event) {
270: if (event.detail == DND.DROP_MOVE) {
271: /*
272: * Having moved the entry, we must delete this one.
273: * However, this is done as a single operation with
274: * the merge process (so it all appears as a single
275: * undoable/redoable operation). Thus we have
276: * nothing to do here.
277: */
278: }
279: }
280: });
281:
282: fUnreconciledEntriesControl
283: .addDisposeListener(new DisposeListener() {
284: public void widgetDisposed(DisposeEvent e) {
285: dragSource.dispose();
286: }
287: });
288:
289: return cellControl;
290: }
291:
292: @Override
293: public void createHeaderControls(Composite parent,
294: EntryData entryData) {
295: // All CellBlock implementations must create a control because
296: // the header and rows must match.
297: // Maybe these objects could just point to the header
298: // controls, in which case this would not be necessary.
299: // Note also we use Label, not an empty Composite,
300: // because we don't want a preferred height that is higher
301: // than the labels.
302: new Label(parent, SWT.NONE);
303: }
304: };
305:
306: IndividualBlock<EntryData, Composite> transactionDateColumn = PropertyBlock
307: .createTransactionColumn(TransactionInfo
308: .getDateAccessor());
309: CellBlock<EntryData, BaseEntryRowControl> debitColumnManager = DebitAndCreditColumns
310: .createDebitColumn(fPage.getAccount().getCurrency());
311: CellBlock<EntryData, BaseEntryRowControl> creditColumnManager = DebitAndCreditColumns
312: .createCreditColumn(fPage.getAccount().getCurrency());
313: CellBlock<EntryData, BaseEntryRowControl> balanceColumnManager = new BalanceColumn(
314: fPage.getAccount().getCurrency());
315:
316: /*
317: * Setup the layout structure of the header and rows.
318: */
319: Block<EntryData, EntryRowControl> rootBlock = new HorizontalBlock<EntryData, EntryRowControl>(
320: reconcileButton,
321: transactionDateColumn,
322: PropertyBlock.createEntryColumn(EntryInfo
323: .getValutaAccessor()),
324: PropertyBlock.createEntryColumn(EntryInfo
325: .getCheckAccessor()),
326: PropertyBlock.createEntryColumn(EntryInfo
327: .getMemoAccessor()),
328: new OtherEntriesButton(
329: new HorizontalBlock<Entry, SplitEntryRowControl>(
330: new SingleOtherEntryPropertyBlock(
331: EntryInfo.getAccountAccessor()),
332: new SingleOtherEntryPropertyBlock(
333: EntryInfo.getMemoAccessor(),
334: JMoneyPlugin
335: .getResourceString("Entry.description")),
336: new SingleOtherEntryPropertyBlock(
337: EntryInfo.getAmountAccessor()))),
338: debitColumnManager, creditColumnManager,
339: balanceColumnManager);
340:
341: // Create the table control.
342: IRowProvider rowProvider = new ReusableRowProvider(rootBlock);
343: fUnreconciledEntriesControl = new EntriesTable<EntryData>(
344: getSection(), toolkit, rootBlock,
345: unreconciledTableContents, rowProvider, fPage
346: .getAccount().getSession(),
347: transactionDateColumn, rowTracker) {
348: @Override
349: protected EntryData createEntryRowInput(Entry entry) {
350: return new EntryData(entry, session.getDataManager());
351: }
352:
353: @Override
354: protected EntryData createNewEntryRowInput() {
355: return new EntryData(null, session.getDataManager());
356: }
357: };
358:
359: getSection().setClient(fUnreconciledEntriesControl);
360: toolkit.paintBordersFor(fUnreconciledEntriesControl);
361: refresh();
362: }
363:
364: public void reconcileEntry(EntryRowControl rowControl) {
365: if (fPage.getStatement() != null) {
366: Entry entry = rowControl.getUncommittedTopEntry();
367:
368: // TODO: What do we do about the blank entry???
369:
370: /*
371: * It is possible that the user has made changes to this entry
372: * that have not yet been committed. Furthermore, those changes
373: * may have put the entry into an invalid state that prevents them
374: * from being committed.
375: *
376: * As validation is done at commit time, we can only set the entry as
377: * reconciled and then attempt to commit it.
378: */
379:
380: // The EntriesTree control will always validate and commit
381: // any outstanding changes before firing a default selection
382: // event. We set the property to put the entry into the
383: // statement and immediately commit the change.
384: if (entry != null) {
385: entry.setPropertyValue(ReconciliationEntryInfo
386: .getStatementAccessor(), fPage.getStatement());
387:
388: /*
389: * We tell the row control to commit its changes. These changes
390: * include the above change. They may also include prior changes
391: * made by the user.
392: *
393: * The tables and controls in this editor should all be capable of
394: * updating themselves correctly when the change is committed. There
395: * is a listener that is listening for changes to the committed data
396: * and this listener should ensure all is updated appropriately,
397: * just as though the change came from outside this view. However,
398: * we must go through the row control to commit the changes. This
399: * ensures that the row control knows that its changes are being
400: * committed and it does not get confused when the listener
401: * processes the changes.
402: */
403: rowControl.commitChanges("Reconcile Entry");
404: }
405: } else {
406: MessageDialog
407: .openError(
408: getSection().getShell(),
409: "Action is Not Available",
410: "You must select a statement first before you can reconcile an entry. The entry will then reconcile to the statement in the upper table.");
411: }
412: }
413: }
|