Source code: com/pjsofts/eurobudget/beans/Transaction.java
1 package com.pjsofts.eurobudget.beans;
2
3 import com.pjsofts.eurobudget.EBConstants;
4 import com.pjsofts.eurobudget.EuroBudget;
5 import java.awt.datatransfer.DataFlavor;
6 import java.awt.datatransfer.Transferable;
7 import java.awt.datatransfer.UnsupportedFlavorException;
8 import java.io.ByteArrayInputStream;
9 import java.io.IOException;
10 import java.io.Serializable;
11 import java.util.*;
12 import java.util.logging.Level;
13 import javax.swing.Icon;
14
15 /**
16 * Java Bean
17 * Operation, Payment ...
18 * Any move of money managed by this software
19 * Can't be abstract cause Bean but this class should normally never be instanciated directly.
20 * (except to create some tmp transaction that serves to make a search in a list of txns)
21 */
22 public class Transaction extends Object implements java.lang.Comparable, Transferable, Serializable
23 {
24 private static final long serialVersionUID = 2398412327203363556L;
25 private static final transient ResourceBundle i18n = EBConstants.i18n;
26
27 // properties names used by property support
28 public static final transient String PROPERTY_AMOUNT = "amount";
29 public static final transient String PROPERTY_DATE = "date";
30 public static final transient String PROPERTY_EFFECTIVE_DATE = "effectivedate";
31 public static final transient String PROPERTY_CREATION_DATE = "creationdate";
32 public static final transient String PROPERTY_CATEGORYPAIR = "categorypair";
33 public static final transient String PROPERTY_CATEGORY = "category";
34 public static final transient String PROPERTY_SUBCATEGORY = "subcategory";
35 public static final transient String PROPERTY_DETAILS = "details";
36 public static final transient String PROPERTY_NUMBER = "number";
37 //public static final String PROPERTY_ENTITY = "entity";
38 public static final transient String PROPERTY_TO_ENTITY = "toentity";
39 public static final transient String PROPERTY_FROM_ENTITY = "fromentity";
40 public static final transient String PROPERTY_ACCOUNT = "account";
41 public static final transient String PROPERTY_CHECKED = "checked";
42 public static final transient String PROPERTY_VERIFIED = "verified";
43 public static final transient String PROPERTY_SRC_ACCOUNT = "sourceaccount";
44 public static final transient String PROPERTY_TARGET_ACCOUNT = "targetaccount";
45 // not really properties but some shortcuts
46 public static final transient String PROPERTY_DIRECTION = "direction";
47 public static final transient String PROPERTY_ENTITY = "entity";
48 public static final transient String PROPERTY_TYPE = "type";
49 public static final transient String PROPERTY_TYPE_ICON = "typeicon";
50 public static final transient String PROPERTY_SPLITTED = "split";
51
52 public static final transient DataFlavor DATA_FLAVOR = new DataFlavor(Transaction.class,"Transaction");
53
54 /** used to reset dates */
55 private static final transient Calendar cal = Calendar.getInstance();
56
57
58 /** Holds value of property date.(Time should be reset to same value for all date)
59 * @serial
60 */
61 private Date date = null;
62 /** Date on which the amount is really on our account
63 * @serial
64 */
65 private Date effectiveDate= null;
66 /** Date & Time on which the user enter this transaction (date de saisie, util pour qq tri)
67 * @serial
68 */
69 private Date creationDate = null;
70
71 /** Holds value of property amount.
72 * @serial
73 */
74 private double amount = 0.0d;
75
76 /** Holds value of property verified.
77 * It means owner make verification from the bank monthly receipe until this item
78 * @serial
79 */
80 private boolean verified = false;
81
82 /** Holds value of property checked.
83 * It means if this transaction has been checked by the owner (thanks to a ticket,note or memory)
84 * @serial
85 */
86 private boolean checked = false;
87
88 /** Holds value of property libelle.(getting info from bank file)
89 * Nature de l'opération
90 */
91 //private String libelle;
92
93 /** Holds value of property details. (comment, details d'écriture )
94 * Commentaire ou détails
95 * @serial
96 */
97 private String details = null;
98
99 /** may be used later on ...
100 * @serial
101 */
102 private transient BitSet flags = null;
103
104 /** Holds value of property category. (revenues/costs)
105 * Warning each txn should have its own instance of CategoryPair
106 * (or we will have bad bugs !!)
107 * @serial
108 */
109 private CategoryPair categoryPair = new CategoryPair();
110
111 /** another category to look in many other ways , not used yet
112 */
113 private transient Category[] classification; // or CategoryPair ???
114
115 /** contains other transaction (for split txns) */
116 private List txnList = null;//save memory ...//new ArrayList();
117
118 /** default constructor for a bean
119 * set the creation date to now
120 */
121 public Transaction(){
122 super();
123 this.creationDate = new Date();
124 }
125
126 /** copy/convertor constructor (this accepts to create a Check from a Virement !)*/
127 public Transaction(Transaction t){
128 this();
129 this.copy(t); // copy also the creation date ??
130 }
131
132 /** Not a real copy constructor, but a convertor
133 * need to be overridden by each child (that add a property)
134 * this may not copy all properties to respect our human rules...
135 * Used in convertion action of Payment to Versement ..
136 */
137 public void copy(Transaction t) {
138 setDate(t.getDate());
139 setAmount(t.getAmount());
140 setDetails(t.getDetails());
141 setChecked(t.isChecked());
142 setVerified(t.isVerified());
143 setEffectiveDate(t.getEffectiveDate());
144 //setEntityName(t.getEntityName());
145 //warning categories may be wrong if change from cost to revenue (payment to versement)
146 // setCategory(t.getCategory());
147 // setSubCategory(t.getSubCategory());
148 // copy also the creation date ?? not possible with final
149 //this.creationDate = t.getCreationDate();
150 }
151
152 /**
153 * Copy field that are null or empty for a template transaction.
154 * (Works on details, category pair, amount)
155 * need to be overridden by each child (that add a property)
156 * this may not copy all properties to respect our human rules...
157 */
158 public void copyAutoEntry(Transaction t) {
159 // doesn't copy any date property
160 if ( this.getDetails() == null || this.getDetails().length() == 0 ) {
161 this.setDetails(t.getDetails());
162 }
163 if ( this.getCategoryPair() == null )
164 this.setCategoryPair(new CategoryPair());
165 if ( this.getCategoryPair().isNull() ) {
166 this.getCategoryPair().copy(t.getCategoryPair());// problem here (how to set a ctg ?)
167 }
168 if ( this.getAmount() == 0d ) {
169 this.setAmount(t.getAmount());
170 }
171 }
172
173 /**
174 * @return true if transaction is splitted in sub-txn */
175 public boolean isSplitted() {
176 return this.txnList != null && !this.txnList.isEmpty();
177 }
178
179 /** convenient constructor
180 * used by child public constructor but here should not be public */
181 protected Transaction(Date date, double amount, String details,
182 CategoryPair cat) {
183 this();
184 setDate(date);
185 setAmount(amount);
186 //setLibelle(lib);
187 setDetails(details);
188 setCategoryPair(cat);
189 }
190
191 /** Getter for property date.
192 * @return Value of property date.
193 */
194 public Date getDate() {
195 return this.date;
196 }
197
198 /** Setter for property date.
199 * @param date New value of property date. will remove the time part (set to Zero hour,mn,sec)
200 * if null is given, set to new Date !
201 */
202 public void setDate(Date date) {
203 if ( date == null ) date = new Date();
204 synchronized (cal) {
205 cal.setTime(date);
206 cal.set(Calendar.HOUR,0);
207 cal.set(Calendar.MINUTE,0);
208 cal.set(Calendar.SECOND,0);
209 cal.set(Calendar.MILLISECOND,0);
210 this.date = cal.getTime();
211 }
212 if ( getEffectiveDate() == null )
213 setEffectiveDate(this.date);
214 }
215
216 /** Getter for property amount.
217 * @return Value of property amount.
218 */
219 public double getAmount() {
220 return this.amount;
221 }
222
223 /** Setter for property amount.
224 * @param amount New value of property amount.
225 */
226 public void setAmount(double amount) {
227 this.amount = Math.abs(amount);//amount;
228 }
229
230 /** Getter for property verified.
231 * @return Value of property verified.
232 */
233 public boolean isVerified() {
234 return this.verified;
235 }
236
237 /** Setter for property verified.
238 * @param verified New value of property verified.
239 */
240 public void setVerified(boolean verified) {
241 this.verified = verified;
242 }
243
244 /** Getter for property checked.
245 * @return Value of property checked.
246 */
247 public boolean isChecked() {
248 return this.checked;
249 }
250
251 /** Setter for property checked.
252 * @param checked New value of property checked.
253 */
254 public void setChecked(boolean checked) {
255 this.checked = checked;
256 }
257
258 // /** Getter for property libelle.
259 // * @return Value of property libelle.
260 // */
261 // public String getLibelle() {
262 // return this.libelle;
263 // }
264 //
265 // /** Setter for property libelle.
266 // * @param libelle New value of property libelle.
267 // */
268 // public void setLibelle(String libelle) {
269 // this.libelle = libelle;
270 // }
271
272 /** Getter for property details.
273 * @return Value of property details.
274 */
275 public String getDetails() {
276 return this.details;
277 }
278
279 /** Setter for property details.
280 * @param details New value of property details.
281 */
282 public void setDetails(String details) {
283 this.details = details;
284 }
285
286 /** Getter for property category.
287 * @return Value of property category.
288 */
289 public CategoryPair getCategoryPair() {
290 return this.categoryPair;
291 }
292
293 /**
294 * Don't use this call normally (use copyCategoryPair instead) to avoid transactions to share
295 * the same category pair.
296 * Setter for property category.
297 * warning, used for serialization but if you need to copy a category pair or change its main or sub ctg, use other methods.
298 * (like getCategoryPair().setMain(categoryPair.getMain()))
299 * @param category New value of property category.
300 *
301 */
302 // public void setCategoryPair(CategoryPair categoryPair) {
303 // this.categoryPair = categoryPair;
304 // }
305
306 /**
307 * Complex behavior:
308 * - if given pair is special set to it
309 * - if current pair is null or special create a new one
310 * - if given pair is not special, just copy it.
311 * Setter for property category.
312 * warning, used for serialization but if you need to copy a category pair or change its main or sub ctg, use other methods.
313 * (like getCategoryPair().setMain(categoryPair.getMain()))
314 * @param category New value of property category.
315 *
316 */
317 public void setCategoryPair(CategoryPair categoryPair) {
318 if ( categoryPair == null ) {
319 this.categoryPair = null;
320 } else if ( categoryPair.isSpecial() ) {
321 // try to keep only one instance of special pair
322 this.categoryPair = categoryPair;
323 } else {
324 if ( this.categoryPair == null || this.categoryPair.isSpecial() ) {
325 this.categoryPair = new CategoryPair();
326 }
327 this.categoryPair.copy(categoryPair);
328 }
329 }
330
331
332 /** not static as it should be override
333 * @return short string representing this kind of transaction */
334 public String getType() {
335 return i18n.getString("Transaction");
336 }
337
338 /**
339 * @return icon representing this kind of transaction
340 * warning implementation should try to refer to same instance of icon whenever possible.
341 */
342 public Icon getTypeIcon() {
343 return null;
344 }
345
346 /** @return string representing what should be in the "to/from" column of txn table
347 * null meaning the owner
348 */
349 public String getDirectionTip() {
350 return null;
351 }
352
353 /** @return the real amount eg. the one which service in computation of balance
354 * per default the negative amount
355 */
356 public double getRealAmount() {
357 return -getAmount();
358 }
359
360 /** @return Long number that should identify this transaction (check number or else) or null if no info
361 */
362 public Long getID() {
363 return null;
364 }
365
366 /** Getter for property effective_date.
367 * @return Value of property effective_date.
368 */
369 public Date getEffectiveDate() {
370 return effectiveDate;
371 }
372
373 /** Setter for property effective_date.
374 * @param effective_date New value of property effective_date.
375 */
376 public void setEffectiveDate(Date effective_date) {
377 this.effectiveDate = effective_date;
378 }
379 /**
380 * Note: this class has a natural ordering that is
381 * inconsistent with equals !
382 * should accept date null values, null means before anything
383 * Ordered on : Date,CreationDate
384 * @return 1: obj<this,; 0 equals(exact same txn), -1: this<obj
385 * Warning: if == 0 doesn't mean same txn then many algorythm will fail (go to, search,contains...)
386 */
387 public int compareTo(Object obj) {
388 if ( obj instanceof Transaction ) {
389 Transaction tobj = (Transaction)obj;
390 if ( obj == null || tobj.getDate() == null ) return 1;
391 if ( this.getDate() == null ) return -1;
392 // look for same date (yy,mm,dd) is not done as time should have resetted for all dates
393 int compDate = this.getDate().compareTo(tobj.getDate());
394 if ( compDate != 0 ) return compDate;
395 //same date, compare on Creation date
396 if ( tobj.getCreationDate() == null ) return 1;
397 if ( this.getCreationDate() == null ) return -1;
398 int compWDate = this.getCreationDate().compareTo(tobj.getCreationDate());
399 //should not happen normally (each txn should have its own creation date but
400 // to avoid bugs we put a last ordering on hashcode...
401 if ( compWDate != 0 ) return compWDate;
402 else if ( this.hashCode() < tobj.hashCode() ) return -1;
403 else if ( this.hashCode() > tobj.hashCode() ) return 1;
404 else return 0;//in that case same instance !
405 } else {
406 throw new java.lang.IllegalArgumentException(i18n.getString("error_Compare_only_to_Transaction"));
407 }
408 }
409 /**
410 * Convenient method when iterating over transactions and avoiding instanceof
411 * @return the related entity(if any) or null (if doesn't)
412 * only Payment and Versment should return something != null
413 * Note that Transaction doesn't have any entity property
414 */
415 public Entity getEntity(){
416 return null;
417 }
418
419 /**
420 * Convenient method when iterating over transactions and avoiding instanceof
421 * @return name of the related entity(person) or null if doesn)
422 * only Payment and Versment should return something != null
423 * Note that Transaction doesn't have any entityName property
424 */
425 public String getEntityName(){
426 return null;
427 }
428
429 /** Getter for property CreationDate.
430 * @return Value of property CreationDate.
431 */
432 public Date getCreationDate() {
433 return creationDate;
434 }
435
436 /**
437 * Should not be called manually, only serve to reload a bean.
438 * Setter for property creationDate.
439 * @param creationDate New value of property creationDate.
440 */
441 public void setCreationDate(Date creationDate) {
442 this.creationDate = creationDate;
443 }
444
445 /** @return a cvs string (text data separated with ;) */
446 public String toStringCSV() {
447 // separated with ; or ,
448 // ? replace simple quote by double quote if needed ... we assume its not neccesary yet
449 return toString("\"", "\";", false);
450 }
451
452 /** @return an other excel format tab text format*/
453 public String toStringTAB() {
454 return toString("", "\t", false);
455 }
456
457 /** @return text/xml format*/
458 public String toStringXML() {
459 return toString("", "\n", true);
460 }
461
462 /**
463 * @param p prefix
464 * @param s suffix
465 * @return text describing each fields, with prefix and suffix string
466 */
467 private String toString(String p, String s, boolean xml) {
468 // separated with tab
469 StringBuffer sb = new StringBuffer();
470 append(sb,xml,PROPERTY_DATE,getDate(),p,s);
471 append(sb,xml,PROPERTY_AMOUNT,new Double(getRealAmount()),p,s);
472 append(sb,xml,PROPERTY_TYPE,getType(),p,s);
473 append(sb,xml,PROPERTY_FROM_ENTITY,getEntityName(),p,s);
474 append(sb,xml,PROPERTY_CATEGORYPAIR,getCategoryPair(),p,s);
475 append(sb,xml,PROPERTY_NUMBER,getID(),p,s);
476 append(sb,xml,PROPERTY_DETAILS,getDetails(),p,s);
477 append(sb,xml,PROPERTY_DIRECTION,getDirectionTip(),p,s);
478 append(sb,xml,PROPERTY_EFFECTIVE_DATE,getEffectiveDate(),p,s);
479 append(sb,xml,PROPERTY_CHECKED,(isChecked() ? i18n.getString("Checked") : i18n.getString("Unchecked")),p,s);
480 append(sb,xml,PROPERTY_VERIFIED,(isVerified()? i18n.getString("Verified") : i18n.getString("Unverified")),p,s);
481 append(sb,xml,PROPERTY_SPLITTED,(isSplitted()? i18n.getString("Splitted") : i18n.getString("Unsplitted")),p,s);
482 append(sb,xml,PROPERTY_CREATION_DATE,getCreationDate(),p,s);
483 return sb.toString();
484 }
485
486 /**
487 *
488 * param tag name of xml tag
489 */
490 private void append(StringBuffer sb, boolean xml, String tag, Object value, String prefix, String suffix) {
491 sb.append(prefix);
492 if ( xml ) sb.append("<").append(tag).append(">");
493 sb.append(String.valueOf(value));
494 if ( xml ) sb.append("</").append(tag).append(">");
495 sb.append(suffix);
496 }
497
498 /**
499 * Returns an object which represents the data to be transferred. The class
500 * of the object returned is defined by the representation class of the flavor.
501 *
502 * @param flavor the requested flavor for the data
503 * @see DataFlavor#getRepresentationClass
504 * @exception IOException if the data is no longer available
505 * in the requested flavor.
506 * @exception UnsupportedFlavorException if the requested data flavor is
507 * not supported.
508 */
509 public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
510 if ( !isDataFlavorSupported(flavor) ) {
511 throw new UnsupportedFlavorException(flavor);
512 }
513 if ( flavor.getRepresentationClass().equals(Transaction.class) ) {
514 // //representationClass = representationClass
515 // //mimeType = application/x-java-serialized-object
516 return this.getPasteClone();
517 } else if ( flavor.isMimeTypeEqual("text/csv") ) {
518 String s = this.toStringCSV();
519 // StringReader sr = new StringReader(s);
520 // sr.read(
521 // new InputStream().
522 return new ByteArrayInputStream(s.getBytes("UTF-8"));
523 } else if ( flavor.isMimeTypeEqual("text/xml") ) {
524 return new ByteArrayInputStream(this.toStringXML().getBytes("UTF-8"));
525 // Not easy to create :
526 // } else if ( flavor == DataFlavor.getTextPlainUnicodeFlavor()) {//unicode one
527 // //return new ByteArrayInputStream(this.toStringTAB().getBytes());
528 // Charset(System.getProperty("unicode charsets")).getEncoder();
529 // Reader reader = flavor.getReaderForText(this.toStringTAB());
530 // return reader;
531 } else {
532 //just text DataFlavor.stringFlavor
533 return this.toStringTAB();
534 }
535 }
536
537 /**
538 * Returns an array of DataFlavor objects indicating the flavors the data
539 * can be provided in. The array should be ordered according to preference
540 * for providing the data (from most richly descriptive to least descriptive).
541 * Order:
542 * Transaction instance, text/csv, text/plain, String
543 * @return an array of data flavors in which this data can be transferred
544 */
545 public DataFlavor[] getTransferDataFlavors() {
546 Vector v = new Vector();
547 try {
548 v.add(new DataFlavor("text/csv") );
549 v.add(new DataFlavor("text/xml") );
550 } catch (ClassNotFoundException e) {
551 //e.printStackTrace();
552 // try to stay independant of other classes:
553 EuroBudget.logException(e);
554 }
555 //v.add(DataFlavor.getTextPlainUnicodeFlavor());
556 //representationClass = java.io.InputStream
557 // mimetype = "text/plain;charset=<platform default Unicode encoding>"
558 //The Win32 Sun implementations use the encoding utf-16le. The Solaris and Linux Sun implementations use the encoding iso-10646-ucs-2.
559 v.add(DATA_FLAVOR);//in the endf or others apps
560 v.add(DataFlavor.stringFlavor);
561 //representationClass = representationClass
562 //mimeType = application/x-java-serialized-object
563 return (DataFlavor[])v.toArray(new DataFlavor[v.size()]);
564 }
565
566 /**
567 * Returns whether or not the specified data flavor is supported for
568 * this object.
569 * @param flavor the requested flavor for the data
570 * @return boolean indicating whether or not the data flavor is supported
571 */
572 public boolean isDataFlavorSupported(DataFlavor flavor) {
573 List list = Arrays.asList(getTransferDataFlavors());
574 return list.contains(flavor);
575 }
576
577 /** Getter for property flags.
578 * @return Value of property flags.
579 */
580 public BitSet getFlags() {
581 return flags;
582 }
583
584 /** Setter for property flags.
585 * @param flags New value of property flags.
586 */
587 public void setFlags(BitSet flags) {
588 this.flags = flags;
589 }
590
591 /** Getter for property classification.
592 * @return Value of property classification.
593 */
594 public com.pjsofts.eurobudget.beans.Category[] getClassification() {
595 return this.classification;
596 }
597
598 /** Setter for property classification.
599 * @param classification New value of property classification.
600 */
601 public void setClassification(com.pjsofts.eurobudget.beans.Category[] classification) {
602 this.classification = classification;
603 }
604
605 /** Returns a string representation of the object. In general, the
606 * <code>toString</code> method returns a string that
607 * "textually represents" this object. The result should
608 * be a concise but informative representation that is easy for a
609 * person to read.
610 * It is recommended that all subclasses override this method.
611 * <p>
612 * The <code>toString</code> method for class <code>Object</code>
613 * returns a string consisting of the name of the class of which the
614 * object is an instance, the at-sign character `<code>@</code>', and
615 * the unsigned hexadecimal representation of the hash code of the
616 * object. In other words, this method returns a string equal to the
617 * value of:
618 * <blockquote>
619 * <pre>
620 * getClass().getName() + '@' + Integer.toHexString(hashCode())
621 * </pre></blockquote>
622 *
623 * @return a string representation of the object.
624 *
625 */
626 public String toString() {
627 String retValue;
628 retValue = super.toString() + "("+toStringCSV()+")";
629 return retValue;
630 }
631
632 /** Getter for property txnList.
633 * @return Value of property txnList.
634 *
635 */
636 public java.util.List getTxnList() {
637 return txnList;
638 }
639
640 /** Setter for property txnList.
641 * @param txnList New value of property txnList.
642 *
643 */
644 public void setTxnList(java.util.List txnList) {
645 this.txnList = txnList;
646 }
647
648 /**
649 * Convenient method when iterating over transactions and avoiding instanceof
650 * only Payment and Versment should return something != null
651 * Note that Transaction doesn't have any entityName property
652 */
653 // not good as Entity is not a String and this class can't make a link between the two
654 // public void setEntityName(String name) {
655 // //any child that has one entity only should override this method
656 // }
657
658 /**
659 * @return shallow copy (not deep copy) of this transaction
660 * new transaction won't be checked, neither verified
661 * clone won't have same creation date.
662 */
663 public Transaction getPasteClone() {
664 Transaction t = null;
665 try {
666 t = (Transaction)this.getClass().newInstance();//better than clone()
667 //t.setCreationDate(new Date());
668 t.setChecked(false);
669 t.setVerified(false);
670 t.setDate(getDate());
671 t.setAmount(getAmount());
672 t.setDetails(getDetails());
673 t.setEffectiveDate(getEffectiveDate());
674 //t.setCategoryPair(new CategoryPair());
675 t.getCategoryPair().copy(this.getCategoryPair());
676 if ( this.isSplitted() ) {
677 List newList = getPasteClone(this.getTxnList());
678 t.setTxnList(newList);
679 //System.out.println("oldList= "+this.getTxnList().size()+" new="+t.getTxnList().size());
680 }
681 } catch (Exception cne) {
682 cne.printStackTrace();//no call to EuroBudget.log(Level.CRITICAL, cne);
683 EuroBudget.log(Level.SEVERE, cne);
684 }
685 return t;
686 }
687
688 /**
689 * @param list of Transaction
690 * @return new list of past cloned transaction
691 * (if given list is null, return null)
692 */
693 public static List getPasteClone(List list) {
694 if ( list == null ) return null;
695 List result = new ArrayList(list.size());
696 for (Iterator it=list.iterator(); it.hasNext(); ) {
697 Transaction t = (Transaction)it.next();
698 result.add(t.getPasteClone());
699 }
700 System.out.println("PasteCloneList oldList= "+list.size()+" new="+result.size());
701 return result;
702 }
703 }
704