GnuCash  2.6.99
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
CBBlib.pl
Go to the documentation of this file.
1 #!/usr/bin/perl -w
2 #use Exporter();
3 
4 package CBBlib;
5 
6 use strict;
7 use English;
8 use IO;
9 ## @file
10 # @brief Belongs to package CBBlib
11 #
12 #### To do ######################
13 #
14 # Check remove transactions.
15 # Move everything to Cbb package.
16 #
17 # put warnings into add_txns and remove_txns if attempted when inside
18 # begin/end_txn_modifications
19 #
20 # Check to see that set_db is OK in the face of modifications...
21 #
22 # update_ledger has to return a sorted list of txn indices for modified txns
23 #
24 # ledger_add/modify/remove_txns?
25 #
26 # note_txn_modification
27 #
28 # Dirty should be set whenever a modification is made.
29 # Dirty should be cleared whenever the client tells us we're clear...
30 #
31 # What about clones and begin/end_modify_txns and no db?
32 #
33 # check copy_obj, and automate?
34 #
35 # Need to clone account balances too?
36 #
37 # Should have list positions in txns?
38 #
39 # Need to create "Unitemized" category by default.
40 #
41 # Don't sort modifications by serial number. Order is irrelevant.
42 # Just use dates
43 # @cond Perl
44 
45 STDOUT->autoflush(1);
46 STDERR->autoflush(1);
47 
48 #@ISA = qw(Exporter);
49 #@EXPORT = qw(func1 func2);
50 #@EXPORT_OK = qw($sally @listabob %harry func3);
51 
52 ## private #################################################################
53 
54 sub BEGIN {}
55 
56 sub END {
57  my $old_status = $?;
58 
59  $? = $old_status;
60 }
61 
62 ## public ##################################################################
63 
64 my $pref_debug = 0;
65 my $pref_verbose = 0;
66 
67 sub set_debug {
68  my($value) = @_;
69  $pref_debug = 1;
70 }
71 
72 sub set_verbose {
73  my($value) = @_;
74  $pref_verbose = 1;
75 }
76 
77 sub debug {
78  my($message) = @_;
79  print STDERR $message if $pref_debug;
80 }
81 
82 sub verbose {
83  my($message) = @_;
84  print STDERR $message if ($pref_verbose || $pref_debug);
85 }
86 
87 my $elapsed_offset = 0;
88 
89 sub elapsed_reset {
90  my $prefix = shift;
91  my($user,$system) = times;
92  $elapsed_offset = $user + $system;
93  print STDERR $prefix . " ... " if $prefix;
94 }
95 sub elapsed {
96  my $prefix = shift;
97  print STDERR $prefix if $prefix;
98  my($user,$system) = times;
99  print STDERR '(elapsed time: ' . ($user + $system - $elapsed_offset) . ")\n";
100 }
101 
102 package CBBlib::Sink;
103 use strict;
104 use English;
105 use IO;
106 
107 # Abstract class -- no "new" defined.
108 
109 # A destination for money. Parent class for Acct and Cat.
110 
111 # It's really possible that ledgers should be a separate class, but
112 # I'm tired and that's too big a change right now.
113 
114 
115 sub acquire_ledger {
116  my $self = shift;
117  my $ledger = $self->get_ledger_();
118  if(!$ledger) {
119  $self->build_ledger_();
120  print STDERR "Building ledger\n";
121  }
122  $self->set_ledger_usage_count_($self->get_ledger_usage_count_() + 1);
123  return $self->get_ledger_();
124 }
125 
126 sub release_ledger {
127  my $self = shift;
128  my $ledger = $self->get_ledger_();
129  if(!$ledger) {
130  print STERR "Big problem. Released ledger that didn't exist!\n";
131  exit 1;
132  }
133 
134  my $usage_count = $self->get_ledger_usage_count_();
135  if(!$usage_count) {
136  print STERR
137  "Big problem. Released ledger that no-one could be holding!\n";
138  exit 1;
139  }
140 
141  $self->set_ledger_usage_count_($usage_count - 1);
142  if($usage_count == 1) {
143  print STDERR "Killing ledger\n";
144  $self->set_ledger_(undef);
145  }
146 }
147 
148 sub build_ledger_ {
149  my($self) = @_;
150  my $db = $self->get_db();
151  my $transactions = $db->get_txns();
152  my $ledger = $self->get_ledger_();
153  if(!$ledger) {
154  $self->set_ledger_([]);
155  $ledger = $self->get_ledger_();
156  }
157  @$ledger = ();
158 
159  my $cleared_balance = 0;
160  my $total = 0;
161  my $txn;
162  foreach $txn (@$transactions) {
163  my($debit, $credit, $applicable_txn) = $txn->totals_wrt($self);
164  if($applicable_txn) {
165  $total += ($credit - $debit);
166  if($txn->cleared_wrt_p($self)) {
167  $cleared_balance += ($credit - $debit);
168  }
169  push @$ledger, [ $txn, $total ];
170  }
171  }
172  $self->set_cleared_balance_($cleared_balance);
173  $self->set_final_balance_($total);
174 }
175 
176 sub compare_ledger_and_txn_ {
177  my($ledger, $txn) = @_;
178  return($$ledger[0]->get_date() cmp $txn->get_date());
179 }
180 
181 sub match_ledger_and_txn_ {
182  my($ledger, $txn) = @_;
183  return($$ledger[0] == $txn);
184 }
185 
186 my $started_mods_p;
187 my $mods_acct;
188 my $cleared_balance_diff_tmp;
189 
190 sub handle_ledger_entry_merge_ {
191  my($ledger, $index, $new_p) = @_;
192 
193  if($new_p || $started_mods_p) {
194  if($new_p) {
195  # convert txn to ledger pair.
196  my $txn = $$ledger[$index];
197  $$ledger[$index] = [$txn, 0.0];
198  $started_mods_p = 1;
199  }
200 
201  my $entry = $$ledger[$index];
202  my $txn = $$entry[0];
203  my($debit, $credit, $applicable_txn) = $txn->totals_wrt($mods_acct);
204 
205  if($new_p) {
206  if($txn->cleared_wrt_p($mods_acct)) {
207  $cleared_balance_diff_tmp += ($credit - $debit);
208  }
209  }
210 
211  my $prev_total = 0;
212  my $prev_entry = $$ledger[$index - 1];
213  if($prev_entry) {
214  $prev_total = $$prev_entry[1];
215  }
216  $$entry[1] = $prev_total + ($credit - $debit);
217  }
218 }
219 
220 my $ledger_removal_diff;
221 
222 sub handle_ledger_entry_removal_ {
223  my($killed_p, $ledger, $index, $old_item) = @_;
224 
225  if($killed_p || $started_mods_p) {
226  my $txn = $$ledger[$index]; $txn = $$txn[0];
227  if($killed_p) {
228  my($debit, $credit, $applicable_txn) = $txn->totals_wrt($mods_acct);
229  $ledger_removal_diff += ($credit - $debit);
230  if($txn->cleared_wrt_p($mods_acct)) {
231  $cleared_balance_diff_tmp -= ($credit - $debit);
232  }
233  $started_mods_p = 1;
234  } else {
235  my $prev_total = 0;
236  my $prev_entry = $$ledger[$index - 1];
237  if($prev_entry) { $prev_total = $$prev_entry[1]; }
238  my $entry = $$ledger[$index];
239  $$entry[1] = $prev_total - $ledger_removal_diff;
240  }
241  }
242 }
243 
244 sub ledger_add_txns_ {
245  my($self, $txns) = @_;
246 
247  my $ledger = $self->get_ledger_();
248  if(!$ledger) {
249  return [];
250  }
251 
252  # Globals, yuck...
253  $cleared_balance_diff_tmp = 0;
254  $started_mods_p = 0;
255  $mods_acct = $self;
256  CBBlib::debug("There are " . scalar(@$ledger) . " ledger entries\n");
257  CBBlib::debug(" adding " . scalar(@$txns) . " ledger entries\n");
258  $$txns[0]->print(\*STDERR);
259  my $added_indices =
260  main::destructive_merge_mangle($ledger, $txns,
261  \&compare_ledger_and_txn_,
262  \&handle_ledger_entry_merge_);
263  $self->set_cleared_balance_($self->get_cleared_balance() +
264  $cleared_balance_diff_tmp);
265  my $final = $$ledger[$#$ledger];
266  $self->set_final_balance_($$final[1]);
267  return $added_indices;
268 }
269 
270 sub ledger_modify_txns_ {
271  my($self, $txn_date_changed, $txns) = @_;
272  # presumes @$txns is sorted on date to match the database order.
273 
274  my $ledger = $self->get_ledger_();
275  if(!$ledger) {
276  return ([], []);
277  }
278 
279  # Brute force date change handling. We should do better later...
280  my @txns_w_date_changes = grep {
281  $$txn_date_changed{$_};
282  } @$txns;
283 
284  my @moves = ();
285 
286  if(@txns_w_date_changes) {
287  print STDERR "Handling ledger mods for date changes\n";
288 
289  # Get the current positions of the changed $txns
290  my %move_from_txn;
291  my @candidates = @txns_w_date_changes;
292  my $candidate = shift @candidates;
293  # Unfortunately, these txns won't be in the order relative to the
294  # old ledger since their dates have changed, so we have to loop.
295  # This may be marginally faster than just traversing once per txn.
296  while($candidate) {
297  print STDERR "Looking for candidate $candidate\n";
298  my $initial_value = $candidate;
299  my $i;
300  for($i=0; $candidate && $i < scalar(@$ledger); $i++) {
301  my $entry = $$ledger[$i];
302  my $txn = $$entry[0];
303  print STDERR "[$txn] [$candidate]\n";
304  if($txn == $candidate) {
305  my $move = [$i];
306  push @moves, $move;
307  $move_from_txn{$txn} = $move;
308  $candidate = shift @candidates;
309  }
310  }
311  if($initial_value == $candidate) {
312  die "Couldn't find transaction in ledger during ledger modify";
313  }
314  }
315 
316  # Just trash the existing one to get the new one with these
317  # transactions in the proper positions.
318  $self->build_ledger_();
319 
320  # Now things should be properly sorted to match the order of
321  # @txns_w_date_changes. We'll leave the extra loop just in case,
322  # but this should go off in one pass.
323  @candidates = @txns_w_date_changes;
324  $candidate = shift @candidates;
325  while($candidate) {
326  my $initial_value = $candidate;
327  my $i;
328  for($i=0; $candidate && $i < scalar(@$ledger); $i++) {
329  my $entry = $$ledger[$i];
330  my $txn = $$entry[0];
331  if($txn == $candidate) {
332  my $move = $move_from_txn{$txn};
333  die "FATAL: Couldn't find move in ledger search" if ! $move;
334  push @$move, $i;
335  $candidate = shift @candidates;
336  }
337  }
338  if($initial_value == $candidate) {
339  die "Couldn't find transaction in ledger during ledger modify";
340  }
341  }
342  }
343 
344  my @modified_indices = ();
345  my $current_index = 0;
346 
347  if(@$txns) {
348  my @mod_txns = @$txns;
349  my $next_mod_txn = shift @mod_txns;
350  my $prev_ledger_value = 0;
351  my $started_mods = 0;
352  my $cleared_balance_diff = 0;
353  my $diff = 0;
354  my $entry;
355  foreach $entry (@$ledger) {
356  my $txn = $$entry[0];
357  if(defined($next_mod_txn) && $next_mod_txn == $txn) {
358  $started_mods = 1;
359  my $prev_value = $$entry[1];
360  my($debit, $credit, $applicable_txn) = $txn->totals_wrt($self);
361  my $new_value = $prev_ledger_value + ($credit - $debit);
362  my $local_change = $new_value - $prev_value;
363  if($txn->cleared_wrt_p($self)) {
364  $cleared_balance_diff -= ($credit - $debit);
365  }
366  $diff += $local_change;
367  $$entry[1] = $new_value;
368  $next_mod_txn = shift @mod_txns;
369  push @modified_indices, $current_index;
370  } elsif ($started_mods) {
371  $$entry[1] += $diff;
372  }
373  $prev_ledger_value = $$entry[1];
374  $current_index++;
375  }
376  $self->set_cleared_balance_($self->get_cleared_balance() +
377  $cleared_balance_diff);
378  }
379  my $final = $$ledger[$#$ledger];
380  $self->set_final_balance_($$final[1]);
381  return (\@modified_indices, \@moves);
382 }
383 
384 sub ledger_remove_txns_ {
385  my($self, $txns) = @_;
386  # presumes @$txns is sorted on date to match the database order.
387  # returns a list where each item is [$txn, $old_index].
388 
389  my $ledger = $self->get_ledger_();
390  if(!$ledger) {
391  return;
392  }
393 
394  # Globals, yuck...
395  $ledger_removal_diff = 0;
396  $cleared_balance_diff_tmp = 0;
397  $started_mods_p = 0;
398  $mods_acct = $self;
399 
400  my $removed_indices =
401  main::destructive_remove_mangle($ledger,
402  $txns,
403  \&match_ledger_and_txn_,
404  \&handle_ledger_entry_removal_);
405 
406  print STDERR
407  "Finished removing ledger items (" . join(" ", @$removed_indices) .
408  ")\n";
409 
410  $self->set_cleared_balance_($self->get_cleared_balance() +
411  $cleared_balance_diff_tmp);
412  my $final = $$ledger[$#$ledger];
413  $self->set_final_balance_($$final[1]);
414 
415  my @txns_tmp = @$txns;
416  my @result = map {
417  my $txn = shift @txns_tmp;
418  [$_, $txn];
419  } @$removed_indices;
420  return(\@result);
421 }
422 
423 
424 package CBBlib::Acct;
425 use strict;
426 use English;
427 use IO;
428 
429 use vars qw(@ISA);
430 unshift @ISA, qw(CBBlib::Sink);
431 
432 sub new {
433  my $class = shift;
434  my ($db, $name, $notes) = @_;
435  my $self = make_internals_();
436  bless $self, $class;
437 
438  $self->set_db_($db);
439  $self->set_name_($name);
440  $self->set_notes_($notes);
441 
442  return $self;
443 }
444 
445 sub print {
446  my($self, $fh, $prefix, $id_map) = @_;
447  my $name = $self->get_name();
448  my $notes = $self->get_notes();
449  $notes = "" if ! $notes;
450  $prefix = "" unless $prefix;
451  if($id_map) {
452  print $fh $prefix . $$id_map{$self} . "\t$name\t$notes\n";
453  } else {
454  print $fh $prefix . "$self\t$name\t$notes\n";
455  }
456 }
457 
458 
459 package CBBlib::Cat;
460 use strict;
461 use English;
462 use IO;
463 
464 use vars qw(@ISA);
465 unshift @ISA, qw(CBBlib::Sink);
466 
467 sub new {
468  my $class = shift;
469  my ($db, $name, $notes) = @_;
470  my $self = make_internals_();
471  bless $self, $class;
472 
473  $self->set_db_($db);
474  $self->set_name_($name);
475  $self->set_notes_($notes);
476 
477  return $self;
478 }
479 
480 sub print {
481  my($self, $fh, $prefix, $id_map) = @_;
482  my $name = $self->get_name();
483  my $notes = $self->get_notes();
484  $notes = "" if ! $notes;
485  $prefix = "" unless $prefix;
486  if($id_map) {
487  print $fh $prefix . $$id_map{$self} . "\t$name\t$notes\n";
488  } else {
489  print $fh $prefix . "$self\t$name\t$notes\n";
490  }
491 }
492 
493 package CBBlib::Txn;
494 use strict;
495 use English;
496 use IO;
497 
498 sub new {
499  my $class = shift;
500  my ($date, $source, $checkno, $desc, $status) = @_;
501  $status = "" if !$status;
502 
503  my $self = make_internals_();
504  bless $self, $class;
505 
506  $self->set_date_($date);
507 
508  die "CBBlib::Txn new: source must be a CBBlib::Acct."
509  unless (ref($source) eq 'CBBlib::Acct');
510 
511  $self->set_source_($source);
512 
513  $self->set_checkno_($checkno);
514  $self->set_desc_($desc);
515  $self->set_status_($status);
516 
517  return $self;
518 }
519 
520 
521 sub make_clone_ {
522  my ($self) = @_;
523 
524  my $clone = $self->get_clone_();
525  if(!$clone) {
526  $self->set_clone_($self->copy_obj_());
527  $clone = $self->get_clone_();
528  }
529  return $clone;
530 }
531 
532 
533 
534 
535 # All the set/get slot functions are in CBBlib-auto.pl.
536 
537 # "" = irrelevant (for split line with category destination)
538 # " " = new and untouched
539 # "*" = selected from the balance window to tentatively be
540 # cleared (stage one of the balance process)
541 # "x" = cleared (stage two of the balance process)
542 #
543 # "?" = a tentative future (recurring) transaction
544 # "!" = a past (recurring) transaction
545 
546 sub get_status_wrt {
547  my($self, $sink) = @_;
548  my $result;
549  if($self->get_source() == $sink) {
550  $result = $self->get_status();
551  } else {
552  $result = $self->get_transfer_status($sink);
553  }
554  if(!defined($result)) {
555  print STDERR "Undefined status wrt $sink for\n";
556  $self->print(\*STDERR, " ");
557  }
558  return $result;
559 }
560 
561 sub set_status_wrt_ {
562  my($self, $sink, $val) = @_;
563  if($self->get_source() == $sink) {
564  $self->set_status($val);
565  } else {
566  $self->set_transfer_status_($sink, $val);
567  }
568 }
569 
570 sub copy_obj_ {
571  my($self) = shift;
572 
573  my @copy = @$self;
574  my $copy_ref = \@copy;
575  bless $copy_ref, ref($self);
576 
577  my $splits = $copy_ref->get_splits_();
578 
579  # Copy only the spine of the list (so we can do a "diff" later).
580  my @splits_copy = @$splits;
581 
582  $copy_ref->set_splits_(\@splits_copy);
583  return $copy_ref;
584 }
585 
586 
587 sub get_transfer_status {
588  my($self, $sink) = @_;
589  my $splits = $self->get_splits_();
590  my $result;
591  my $split;
592  foreach $split (@$splits) {
593  if($split->get_dest() == $sink) {
594  $result = $split;
595  last;
596  }
597  }
598  undef $split;
599 
600  if(!$result) {
601  return undef;
602  } else {
603  return $result->get_status();
604  }
605 }
606 
607 
608 sub set_transfer_status_ {
609  my($self, $acct, $new_status) = @_;
610  my $splits = $self->get_splits_();
611  my $result;
612  my $split;
613  foreach $split (@$splits) {
614  my $dest = $split->get_dest();
615  if($split->get_dest() == $acct) {
616  $split->set_status($new_status);
617  }
618  }
619 }
620 
621 sub cleared_p_ {
622  my($self) = @_;
623  return $self->get_status() eq 'x';
624 }
625 
626 sub cleared_wrt_p {
627  my($self, $acct) = @_;
628  return $self->get_status_wrt($acct) eq 'x';
629 }
630 
631 sub clear_wrt {
632  my($self, $acct) = @_;
633  $self->set_status_wrt_($acct, 'x');
634 }
635 
636 sub clear_pending_wrt_p {
637  my($self, $acct) = @_;
638  return $self->get_status_wrt($acct) eq '*';
639 }
640 
641 sub clear_pending_wrt {
642  my($self, $acct) = @_;
643  $self->set_status_wrt_($acct, '*');
644 }
645 
646 sub uncleared_wrt_p {
647  my($self, $acct) = @_;
648  return $self->get_status_wrt($acct) eq ' ';
649 }
650 
651 sub unclear_wrt {
652  my($self, $acct) = @_;
653  $self->set_status_wrt_($acct, ' ');
654 }
655 
656 sub void {
657  # ???
658 }
659 
660 sub add_split {
661  my($self, $split, $insert_position) = @_;
662 
663  # Don't forget to change modify functions in CBBlib-auto.plp too
664  # whenever you make changes here.
665 
666  my $splits;
667 
668  $split->set_txn_($self);
669 
670  my $db = $self->get_db();
671  if($db) {
672 
673  $db->begin_txn_modifications();
674 
675  my $clone = $self->make_clone_();
676  $splits = $clone->get_splits_();
677 
678  if($insert_position) {
679  splice @$splits, $insert_position, 0, ($split);
680  } else {
681  push @$splits, $split;
682  $insert_position = $#$splits;
683  }
684  } else {
685  $splits = $self->get_splits_();
686 
687  if($insert_position) {
688  splice @$splits, $insert_position, 0, ($split);
689  } else {
690  push @$splits, $split;
691  $insert_position = $#$splits;
692  }
693  }
694 
695  my $i;
696  for($i = $insert_position; $i < scalar(@$splits); $i++) {
697  $$splits[$i]->set_pos_($i);
698  }
699 
700  if($db) {
701  $db->record_txn_modification_($self);
702  $db->end_txn_modifications();
703  }
704 }
705 
706 sub remove_split {
707  my($self, $split) = @_;
708 
709  # Don't forget to change modify functions in CBBlib-auto.plp too
710  # whenever you make changes here.
711 
712  my $db = $self->get_db();
713  my $splits;
714  if($db) {
715  $db->begin_txn_modifications();
716 
717  my $clone = $self->make_clone_();
718  $splits = $clone->get_splits_();
719  } else {
720  $splits = $self->get_splits_();
721  }
722 
723  my $old_index = 0;
724  my $split_found = 0;
725  my $candidate;
726  foreach $candidate (@$splits) {
727  if($candidate == $split) {
728  $split_found = 1;
729  last;
730  }
731  $old_index++;
732  }
733  if(!$split_found) {
734  die "Failed to find split in Txn::remove_split";
735  }
736  splice @$splits, $old_index, 0;
737 
738  my $i;
739  for($i = $old_index; $i < scalar(@$splits); $i++) {
740  $$splits[$i]->set_pos_($i);
741  }
742 
743  if($db) {
744  my $txn = $split->get_txn_();
745  $split->set_txn_(undef);
746  $db->record_txn_modification_($txn);
747  $db->end_txn_modifications();
748  }
749 }
750 
751 sub totals_wrt {
752  my($self, $sink) = @_;
753  # O(n)
754  # Returns (total_debit, total_credit, applicable)
755 
756  my($total_debit, $total_credit, $applicability) = (0, 0, 0);
757  my $splits = $self->get_splits_();
758 
759  my $split;
760  foreach $split (@$splits) {
761  my $dest = $split->get_dest();
762 
763  if($self->get_source() == $sink) {
764  $total_debit += $split->get_debit();
765  $total_credit += $split->get_credit();
766  $applicability = 1;
767  } elsif($dest == $sink) {
768  #} elsif((ref($dest) eq 'CBBlib::Acct') && ($dest == $sink)) {
769  $total_debit += $split->get_credit();
770  $total_credit += $split->get_debit();
771  $applicability = 1;
772  }
773  }
774  return($total_debit, $total_credit, $applicability);
775 }
776 
777 
778 sub affected_sinks {
779  my($self) = @_;
780  # O(n)
781  # Returns list of affected sinks
782 
783  my $source = $self->get_source();
784  my %result = ($source => $source);
785  my $splits = $self->get_splits_();
786 
787  map {
788  my $dest = $_->get_dest();
789  if(!$dest) {
790  die "\nNo dest in $_\n";
791  }
792  $result{$dest} = $dest;
793  } @$splits;
794  return(values(%result));
795 }
796 
797 
798 sub print {
799  my($self, $fh, $prefix, $id_map) = @_;
800  $prefix = "" if ! $prefix;
801 
802  print $fh $prefix . $self->get_date() . "\t";
803  if($id_map) {
804  print $fh $$id_map{$self->get_source()} . "\t";
805  } else {
806  print $fh $self->get_source() . "\t";
807  }
808  print $fh $self->get_checkno() . "\t";
809  print $fh $self->get_desc() . "\t";
810  print $fh $self->get_status() . "\n";
811  my $splits = $self->get_splits_();
812  my $split;
813  foreach $split (@$splits) {
814  $split->print($fh, $prefix . ' ', $id_map);
815  }
816 }
817 
818 sub print_pretty {
819  my($self, $fh, $prefix) = @_;
820  $prefix = "" if ! $prefix;
821 
822  print $fh $prefix . $self->get_date() . ":";
823  print $fh ($self->get_source())->get_name() . ":";
824  print $fh $self->get_checkno() . ":";
825  print $fh $self->get_desc() . ":";
826  print $fh $self->get_status() . "\n";
827  my $splits = $self->get_splits_();
828  my $split;
829  foreach $split (@$splits) {
830  $split->print_pretty($fh, $prefix . " ");
831  }
832 }
833 
834 package CBBlib::Split;
835 use strict;
836 use English;
837 use IO;
838 
839 sub new {
840  my $class = shift;
841  my ($dest, $notes, $debit, $credit, $status) = @_;
842  my $self = make_internals_();
843  bless $self, $class;
844 
845  $status = '' if ! $status;
846 
847  $self->set_dest_($dest);
848  $self->set_notes_($notes);
849  $self->set_debit_($debit);
850  $self->set_credit_($credit);
851  $self->set_status_($status);
852 
853  return $self;
854 }
855 
856 sub copy_obj_ {
857  my($self) = shift;
858 
859  my @copy = @$self;
860  my $copy_ref = \@copy;
861  bless $copy_ref, ref($self);
862  return $copy_ref;
863 }
864 
865 sub make_clone_ {
866  my ($self) = @_;
867 
868  my $clone = $self->get_clone_();
869  if(!$clone) {
870  my $txn = $self->get_txn_();
871  if($txn) {
872  $txn->make_clone_();
873  }
874  $self->set_clone_($self->copy_obj_());
875  $clone = $self->get_clone_();
876  }
877  return $clone;
878 }
879 
880 sub get_db {
881  my $self = shift;
882  my $txn = $self->get_txn();
883  if($txn) {
884  return $txn->get_db();
885  } else {
886  return undef;
887  }
888 }
889 
890 sub cleared_p_ {
891  my($self) = @_;
892  my $status = $self->get_status();
893  if($status) {
894  return $status eq 'x';
895  } else {
896  return undef;
897  }
898 }
899 
900 
901 sub print {
902  my($self, $fh, $prefix, $id_map) = @_;
903  $prefix = "" if ! $prefix;
904 
905  print $fh $prefix;
906  if($id_map) {
907  print $fh $$id_map{$self->get_dest()} . "\t";
908  } else {
909  print $fh $self->get_dest() . "\t";
910  }
911  print $fh $self->get_notes() . "\t";
912  print $fh $self->get_debit() . "\t";
913  print $fh $self->get_credit() . "\t";
914  print $fh $self->get_status() . "\n";
915 }
916 
917 
918 package CBBlib::Db;
919 use strict;
920 use English;
921 use IO;
922 
923 sub new {
924  my($class, $default_sink) = @_;
925  my $self = make_internals_();
926  bless $self, $class;
927 
928  if($default_sink) {
929  $self->add_sinks([$default_sink]);
930  } else {
931  $default_sink = new CBBlib::Cat($self, '<<unitemized>>', '');
932  $self->add_sinks([$default_sink]);
933  }
934  $self->set_default_sink($default_sink);
935 
936  return $self;
937 }
938 
939 sub clean_p {
940  # Should eventually know the truth, but this is safe at the moment.
941  return 0;
942 }
943 
944 sub add_sinks {
945  my($self, $sinks) = @_;
946 
947  my $accts = $self->get_accts_();
948  my $cats = $self->get_cats_();
949  map {
950  $_->set_db_($self);
951  if(ref($_) eq 'CBBlib::Acct') {
952  push @$accts, $_;
953  } elsif(ref($_) eq 'CBBlib::Cat') {
954  push @$cats, $_;
955  } else {
956  die "Unknown sink type in CBBlib::Db::add_sinks()";
957  }
958  } @$sinks;
959 }
960 
961 sub record_txn_modification_ {
962  my($self, $txn) = @_;
963 
964  my $mod_level = $self->get_modified_txns_level_();
965  if(!$mod_level) {
966  die "Tried to record_txn_modification_ when not in update region.";
967  }
968  my $modified_txns = $self->get_modified_txns_();
969  #my $serial_number = $self->get_modified_txns_serial_num_();
970  #$self->set_modified_txns_serial_num_($serial_number + 1);
971 
972  if(ref($txn) eq 'CBBlib::Split') {
973  $txn = $txn->get_txn_();
974  }
975  $$modified_txns{$txn} = $txn;
976 }
977 
978 sub update_dirty_txns_hash_ {
979  my($self, $dirty_hash, $txn) = @_;
980 
981  my @affected_sinks = $txn->affected_sinks();
982 
983  map {
984  my $sink = $_;
985  if(!$$dirty_hash{$sink}) {
986  $$dirty_hash{$sink} = [$sink, [$txn]];
987  } else {
988  my $list = $$dirty_hash{$sink};
989  $list = $$list[1];
990  push @$list, $txn;
991  }
992  } @affected_sinks;
993 }
994 
995 
996 sub debug_txns_modified_data {
997  my($self, $modifications, $dirty_sinks) = @_;
998  CBBlib::debug("CBBlib CALLBACK: txns-modified\n");
999 
1000  CBBlib::debug(" Modifications:\n");
1001  map {
1002  my $txn = $$_[0];
1003  my $mods = $$_[1];
1004  CBBlib::debug(" [$txn");
1005  map {
1006  CBBlib::debug("\n [");
1007  my $first = 1;
1008  map {
1009  if($first) {
1010  $first = 0;
1011  } else {
1012  print " ";
1013  }
1014  if(!defined($_)) {
1015  print "<<undefined>>";
1016  } else {
1017  print $_;
1018  }
1019  } @$_;
1020  CBBlib::debug("]");
1021  } @$mods;
1022  CBBlib::debug("]\n");
1023  } @$modifications;
1024 
1025  CBBlib::debug(" Affected sinks:\n");
1026  map {
1027  my ($sink, $mod_txns, $indices) = @$_;
1028  $sink->print(\*STDERR, " ");
1029  } values(%$dirty_sinks);
1030 }
1031 
1032 
1033 sub post_modification_notices_ {
1034  my $self = shift;
1035  CBBlib::debug("CBBlib::post_modification_notices_: checking for changes.\n");
1036 
1037  my $modified_txns_hash = $self->get_modified_txns_();
1038  my @sorted_txns = sort {
1039  $a->get_date <=> $b->get_date();
1040  } values(%$modified_txns_hash);
1041  my @modifications = ();
1042 
1043  # Have to treat credit/debit mods the same as other field mods since
1044  # in the end we need the ledger indices of all the txns.
1045  my %dirty_sinks;
1046 
1047  # This is not the most efficient way to go about this, but it's
1048  # easy, and I'm in a hurry. Someone can make it do the merge/remove
1049  # mangle thing later. Be careful, though, you don't want to cause
1050  # spurious add/remove events.
1051  my $date_changed;
1052  my %txn_date_changed;
1053 
1054  # generate modified events for each transaction
1055  my $txn;
1056  foreach $txn (@sorted_txns) {
1057  my $new_txn = $txn->get_clone_();
1058  my $old_splits = $txn->get_splits_();
1059  my $new_splits = $new_txn->get_splits_();
1060  my @txn_mods = ();
1061 
1062  my $i;
1063  for($i=0; $i < scalar(@$old_splits); $i++) {
1064  my $old_item = $$old_splits[$i];
1065  if(! grep { $_ == $old_item } @$new_splits) {
1066  my $old_index = $i;
1067  push @txn_mods, ['split-removed', $old_item, $old_index];
1068  }
1069  }
1070 
1071  for($i=0; $i < scalar(@$new_splits); $i++) {
1072  my $new_item = $$new_splits[$i];
1073  if(! grep { $_ == $new_item } @$old_splits) {
1074  my $new_index = $i;
1075  push @txn_mods, ['split-added', $new_item, $new_index];
1076  }
1077  }
1078 
1079  # This could be much faster with a better algorithm.
1080  my @modified_splits = grep { $_->get_clone_() } @$new_splits;
1081 
1082  if($new_txn->get_date_ ne $txn->get_date_()) {
1083  push @txn_mods, ['date', $txn->get_date_()];
1084  $txn->set_date_($new_txn->get_date_());
1085  $date_changed = 1;
1086  $txn_date_changed{$txn} = $txn;
1087  }
1088  if($new_txn->get_checkno_ ne $txn->get_checkno_()) {
1089  push @txn_mods, ['checkno', $txn->get_checkno_()];
1090  $txn->set_checkno_($new_txn->get_checkno_());
1091  }
1092  if($new_txn->get_desc_ ne $txn->get_desc_()) {
1093  push @txn_mods, ['desc', $txn->get_desc_()];
1094  $txn->set_desc_($new_txn->get_desc_());
1095  }
1096  if($new_txn->get_status_ ne $txn->get_status_()) {
1097  push @txn_mods, ['status', $txn->get_status_()];
1098  $txn->set_status_($new_txn->get_status_());
1099  }
1100 
1101  my $split;
1102  foreach $split (@modified_splits) {
1103  my $new_split = $split->get_clone_();
1104 
1105  my $old_txn = $split->get_txn();
1106  if(defined($old_txn) && ($old_txn == $new_split->get_txn())) {
1107  # This is really a modification to an existing split (is that
1108  # what this test should be doing)?
1109 
1110  my $old_pos = $split->get_pos__();
1111  my $new_pos = $new_split->get_pos__();
1112  if((defined($new_pos) != defined($old_pos)) ||
1113  ($new_pos != $old_pos)) {
1114  push @txn_mods, ['split-modified', $split, 'pos',
1115  $split->get_pos__()];
1116  $split->set_pos__($new_split->get_pos__());
1117  }
1118  if($new_split->get_dest_ != $split->get_dest_()) {
1119  push @txn_mods, ['split-modified', $split, 'dest',
1120  $split->get_dest_()];
1121  $split->set_dest_($new_split->get_dest_());
1122  }
1123  if($new_split->get_notes_ ne $split->get_notes_()) {
1124  push @txn_mods, ['split-modified', $split, 'notes',
1125  $split->get_notes_()];
1126  $split->set_notes_($new_split->get_notes_());
1127  }
1128  if($new_split->get_debit_ != $split->get_debit_()) {
1129  push @txn_mods, ['split-modified', $split, 'debit',
1130  $split->get_debit_()];
1131  $split->set_debit_($new_split->get_debit_());
1132  }
1133  if($new_split->get_credit_ != $split->get_credit_()) {
1134  push @txn_mods, ['split-modified', $split, 'credit',
1135  $split->get_credit_()];
1136  $split->set_credit_($new_split->get_credit_());
1137  }
1138  if($new_split->get_status_ ne $split->get_status_()) {
1139  push @txn_mods, ['split-modified', $split, 'status',
1140  $split->get_status_()];
1141  $split->set_status_($new_split->get_status_());
1142  }
1143  }
1144  $split->set_clone_(undef);
1145  }
1146  $txn->set_splits_($new_splits);
1147  $txn->set_clone_(undef);
1148 
1149  if(@txn_mods) {
1150  $self->update_dirty_txns_hash_(\%dirty_sinks, $txn);
1151  push @modifications, [ $txn, \@txn_mods ];
1152  }
1153  }
1154 
1155  # Re-sort if needed, so the positions will be right for the next
1156  # steps.
1157  if($date_changed) {
1158  print STDERR "Re-sorting txns\n";
1159  my $txns = $self->get_txns();
1160  @$txns = sort {
1161  $a->get_date <=> $b->get_date();
1162  } @$txns;
1163  }
1164 
1165  # Act on dirty ledgers hash.
1166  my $acct_data;
1167  foreach $acct_data (values %dirty_sinks) {
1168  my $acct = $$acct_data[0];
1169  my $acct_txns = $$acct_data[1];
1170  my ($txn_indices, $moves) =
1171  $acct->ledger_modify_txns_(\%txn_date_changed,
1172  $acct_txns);
1173  push @$acct_data, $txn_indices, $moves;
1174  }
1175 
1176  if(@modifications) {
1177 
1178  debug_txns_modified_data($self, \@modifications, \%dirty_sinks);
1179 
1180  # txns-modified callback
1181  my $callbacks_hash = $self->get_callbacks_hash_();
1182  my $callbacks = $$callbacks_hash{'txns-modified'};
1183  my $callback;
1184  foreach $callback (@$callbacks) {
1185  my $func = $$callback[0];
1186  my $args = $$callback[1];
1187 
1188  # @modifications elements are of the form
1189  # [ $txn, [ mod, mod, mod, ...]]
1190 
1191  # %dirty_sinks
1192  # Key: $sink
1193  # Value: [ $sink, @$txns, @$current_indices, @$moves]
1194  # where @$moves is a ref to a list of pairs
1195  # of the form [$prev_pos, $new_pos] sorted on $prev_pos
1196 
1197  &$func($self, \@modifications, \%dirty_sinks, $args);
1198 
1199  }
1200  }
1201 }
1202 
1203 
1204 sub begin_txn_modifications {
1205  my ($self) = shift;
1206  my $mod_level = $self->get_modified_txns_level_();
1207  if(!$mod_level) {
1208  $self->set_modified_txns_({});
1209  #$self->set_modified_txns_serial_num_(0);
1210  }
1211  $self->set_modified_txns_level_($mod_level + 1);
1212 }
1213 
1214 sub end_txn_modifications {
1215  my ($self) = shift;
1216  my $mod_level = $self->get_modified_txns_level_();
1217  if($mod_level == 1) {
1218  $self->post_modification_notices_();
1219  } elsif(!$mod_level) {
1220  die
1221  "Big problem. Db::end_txn_modifications called when not modifying.\n";
1222  }
1223  $self->set_modified_txns_level_($mod_level - 1);
1224 }
1225 
1226 sub get_accts_by_name {
1227  my($self, $name) = @_;
1228  my $accts = $self->get_accts();
1229  my @matches = grep {
1230  if($_) {
1231  $_->get_name() eq $name;
1232  } else {
1233  0;
1234  }
1235  } @$accts;
1236  return \@matches;
1237 }
1238 
1239 sub get_cats_by_name {
1240  my($self, $name) = @_;
1241  my $cats = $self->get_cats();
1242  my @matches = grep {
1243  if($_) {
1244  $_->get_name() eq $name;
1245  } else {
1246  0;
1247  }
1248  } @$cats;
1249  return \@matches;
1250 }
1251 
1252 sub extract_accounts_ {
1253  my($text, $hash) = @_;
1254  my @accounts = split("\n", $text);
1255  return map {
1256  my $acct = $_;
1257  my @fields = split("\t", $acct);
1258  (scalar(@fields) < 4) or die "Wrong number of fields in account.";
1259 
1260  my $name = $fields[1];
1261  my $notes = $fields[2];
1262  $acct = new CBBlib::Acct(undef, $name, $notes);
1263  $$hash{$fields[0]} = $acct;
1264  $acct;
1265  } @accounts;
1266 }
1267 
1268 sub extract_categories_ {
1269  my($text, $hash) = @_;
1270  my @categories = split("\n", $text);
1271  return map {
1272  my $cat = $_;
1273  my @fields = split("\t", $cat);
1274  (scalar(@fields) < 4) or die "Wrong number of fields in category.";
1275 
1276  my $name = $fields[1];
1277  my $notes = $fields[2];
1278  $cat = new CBBlib::Cat(undef, $name, $notes);
1279  $$hash{$fields[0]} = $cat;
1280  $cat;
1281  } @categories;
1282 }
1283 
1284 sub calc_account_totals_only_ {
1285  my($self) = @_;
1286  my $transactions = $self->get_txns();
1287  my $accts = $self->get_accts();
1288  map {
1289  if($_) {
1290  $_->set_cleared_balance_(0);
1291  $_->set_final_balance_(0);
1292  }
1293  } @$accts;
1294 
1295  #my $cleared_balance = 0;
1296  #my $final_balance = 0;
1297 
1298  my $txn;
1299  foreach $txn (@$transactions) {
1300  my $splits = $txn->get_splits_();
1301 
1302  my $split;
1303  foreach $split (@$splits) {
1304 
1305  my $source = $txn->get_source();
1306  my $dest = $split->get_dest();
1307  my $debit = $split->get_debit();
1308  my $credit = $split->get_credit();
1309 
1310  my $cleared_bal;
1311  my $final_bal;
1312  my $diff = $credit - $debit;
1313 
1314  if($txn->cleared_p_()) {
1315  $cleared_bal = $source->get_cleared_balance() + $diff;
1316  $source->set_cleared_balance_($cleared_bal);
1317  }
1318  $final_bal = $source->get_final_balance() + $diff;
1319  $source->set_final_balance_($final_bal);
1320 
1321  if(ref($dest) eq 'CBBlib::Acct' && ($source != $dest)) {
1322  if($split->cleared_p_()) {
1323  $cleared_bal = $dest->get_cleared_balance();
1324  $dest->set_cleared_balance_($cleared_bal - $diff);
1325  }
1326  $final_bal = $dest->get_final_balance() - $diff;
1327  $dest->set_final_balance_($final_bal);
1328  }
1329  }
1330  }
1331 }
1332 
1333 
1334 sub add_callback_ {
1335  my($self, $name, $callback, $user_data) = @_;
1336  my $data = [$callback, $user_data];
1337  my $callbacks_hash = $self->get_callbacks_hash_();
1338  my $txn_callbacks = $$callbacks_hash{$name};
1339  if(!$txn_callbacks) {
1340  $$callbacks_hash{$name} = [];
1341  $txn_callbacks = $$callbacks_hash{$name};
1342  }
1343  push @$txn_callbacks, $data;
1344  return $data;
1345 }
1346 
1347 sub remove_callback_ {
1348  my($self, $name, $callback_id) = @_;
1349  my $callbacks_hash = $self->get_callbacks_hash_();
1350  my $callbacks = $$callbacks_hash{$name};
1351  if(scalar(@$callbacks)) {
1352  @$callbacks = grep { !($_ == $callback_id) } @$callbacks;
1353  }
1354 }
1355 
1356 # add_txn_callback
1357 # Called whenever new transactions are added
1358 # Called with args ($db, $new_txns, $user_data)
1359 
1360 sub add_txns_added_callback {
1361  my($self, $callback, $user_data) = @_;
1362  return $self->add_callback_('txns-added', $callback, $user_data);
1363 }
1364 
1365 
1366 sub remove_txns_added_callback {
1367  my($self, $callback_id) = @_;
1368  $self->remove_callback_('txns-added', $callback_id);
1369 }
1370 
1371 
1372 # add_txn_callback
1373 # Called whenever new transactions are added
1374 # Called with args ($db, $dead_txns, $user_data)
1375 
1376 sub add_txns_removed_callback {
1377  my($self, $callback, $user_data) = @_;
1378  return $self->add_callback_('txns-removed', $callback, $user_data);
1379 }
1380 
1381 
1382 sub remove_txns_removed_callback {
1383  my($self, $callback_id) = @_;
1384  $self->remove_callback_('txns-removed', $callback_id);
1385 }
1386 
1387 sub add_txns_modified_callback {
1388  my($self, $callback, $user_data) = @_;
1389  return $self->add_callback_('txns-modified', $callback, $user_data);
1390 }
1391 
1392 sub remove_txns_modified_callback {
1393  my($self, $callback_id) = @_;
1394  $self->remove_callback_('txns-modified', $callback_id);
1395 }
1396 
1397 sub merge_new_txns_into_main_list_ {
1398  my($self, $new_txns) = @_;
1399  my $txns = $self->get_txns();
1400 
1401  my $added_indices =
1402  main::destructive_merge_mangle($txns, $new_txns, sub {
1403  return $_[0]->get_date() cmp $_[1]->get_date();
1404  });
1405 
1406  map { $_->set_db_($self); } @$new_txns;
1407 
1408  my %affected_sinks;
1409  my $txn;
1410  foreach $txn (@$new_txns) {
1411  my @accts = $txn->affected_sinks();
1412  my $acct;
1413  foreach $acct (@accts) {
1414  my $data = $affected_sinks{$acct};
1415  if(!$data) { $data = $affected_sinks{$acct} = [$acct, []]; }
1416  my $list = $$data[1];
1417  push @$list, $txn;
1418  }
1419  }
1420 
1421  return (\%affected_sinks, $added_indices);
1422 }
1423 
1424 sub merge_new_txns_into_ledger_lists_ {
1425  my($self, $new_txns, $affected_accts) = @_;
1426 
1427  # $affected accts is a hash mapping accounts to [acct,
1428  # relevant_txns] acct is a ref to the account, and relevant txns is
1429  # a ref to a list of the relevant transactions. The transactions
1430  # must be ordered in each list like they are in the global DB.
1431 
1432  # We're going to add the resulting new ledger indices to the hash
1433  # values so we have: [acct, relevant_txns, indices]
1434 
1435  my $data;
1436  foreach $data (values(%$affected_accts)) {
1437  my $acct_ref = $$data[0];
1438  my $txns = $$data[1];
1439  my $added_indices = $acct_ref->ledger_add_txns_($txns);
1440  push @$data, $added_indices;
1441  }
1442 }
1443 
1444 sub add_txns {
1445  my($self, $new_txns) = @_;
1446 
1447  @$new_txns = sort {
1448  $a->get_date() <=> $b->get_date();
1449  } @$new_txns;
1450 
1451  my ($affected_accts, $added_indices) =
1452  $self->merge_new_txns_into_main_list_($new_txns);
1453 
1454  $self->merge_new_txns_into_ledger_lists_($new_txns, $affected_accts);
1455 
1456  my $callbacks_hash = $self->get_callbacks_hash_();
1457  my $txn_callbacks = $$callbacks_hash{'txns-added'};
1458  my $callback;
1459  foreach $callback (@$txn_callbacks) {
1460  my $func = $$callback[0];
1461  my $args = $$callback[1];
1462 
1463  if($main::pref_debug) {
1464  print STDERR
1465  "(txns-added\n" .
1466  " db: $self\n" .
1467  ' added-indices: (' . join("\n" .
1468  ' ', @$added_indices) . ")\n" .
1469  " (affected-accts\n";
1470  my $acct_data;
1471  foreach $acct_data (values(%$affected_accts)) {
1472  my $acct = $$acct_data[0];
1473  my $txns = $$acct_data[1];
1474  my $indices = $$acct_data[2];
1475  print STDERR
1476  " acct: $acct\n" .
1477  ' txns: (' . join("\n" .
1478  ' ', @$txns) . ")\n" .
1479  ' indices: (' . join("\n" .
1480  ' ', @$indices) . ")\n";
1481  }
1482  CBBlib::debug(' args: ' . $args . "))\n");
1483  }
1484  &$func($self, $added_indices, $affected_accts, $args);
1485  }
1486 }
1487 
1488 sub remove_txns_from_ledger_lists_ {
1489  my($self, $dead_txns, $affected_accts) = @_;
1490  # $affected accts is a hash mapping accounts to refs to lists
1491  # of relevant transactions. The transactions must be ordered
1492  # in each list like they are in the global DB.
1493 
1494  # returns a hash from $sink to a listref of [$sink, @$txn_info] where
1495  # @$txn_info is a lists of [$txn, $prev_ledger_index] pairs
1496 
1497  my %result;
1498 
1499  my $acct;
1500  foreach $acct (keys(%$affected_accts)) {
1501  my $data = $$affected_accts{$acct};
1502  my $acct_ref = $$data[0];
1503  my $txns = $$data[1];
1504  my $removal_info = $acct_ref->ledger_remove_txns_($txns);
1505  $result{$acct} = [$acct, $removal_info];
1506  }
1507  return \%result;
1508 }
1509 
1510 sub remove_txns_from_main_list_ {
1511  my($self, $dead_txns) = @_;
1512  my $txns = $self->get_txns();
1513 
1514  my $removed_indices =
1515  main::destructive_remove_mangle($txns, $dead_txns, sub {
1516  return $_[0] == $_[1];
1517  });
1518 
1519  map { $_->set_db_($self); } @$dead_txns;
1520 
1521  my %affected_accts;
1522  my $txn;
1523  foreach $txn (@$dead_txns) {
1524  my @accts = $txn->affected_sinks();
1525  my $acct;
1526  foreach $acct (@accts) {
1527  my $data = $affected_accts{$acct};
1528  if(!$data) { $data = $affected_accts{$acct} = [$acct, []]; }
1529  my $list = $$data[1];
1530  push @$list, $txn;
1531  }
1532  }
1533 
1534  return (\%affected_accts, $removed_indices);
1535 }
1536 
1537 sub remove_txns {
1538  my($self, $dead_txns) = @_;
1539 
1540  @$dead_txns = sort {
1541  $a->get_date() cmp $b->get_date();
1542  } @$dead_txns;
1543 
1544  my ($affected_accts, $removed_db_indices) =
1545  $self->remove_txns_from_main_list_($dead_txns);
1546 
1547  my $ledger_removal_info =
1548  $self->remove_txns_from_ledger_lists_($dead_txns, $affected_accts);
1549 
1550  my $callbacks_hash = $self->get_callbacks_hash_();
1551  my $txn_callbacks = $$callbacks_hash{'txns-removed'};
1552  my $callback;
1553  foreach $callback (@$txn_callbacks) {
1554  my $func = $$callback[0];
1555  my $args = $$callback[1];
1556  &$func($self, $dead_txns,
1557  $removed_db_indices,
1558  $ledger_removal_info,
1559  $args);
1560  }
1561 }
1562 
1563 sub print_sinks {
1564  my ($self, $fh, $id_map) = @_;
1565 
1566  print $fh "#### Accounts ####\n";
1567  my $accts = $self->get_accts();
1568  map { $_->print($fh, '', $id_map); } @$accts;
1569  undef $accts;
1570  print $fh "\n";
1571 
1572  print $fh "#### Categories ####\n";
1573  my $cats = $self->get_cats();
1574  map { $_->print($fh, '', $id_map); } @$cats;
1575  undef $cats;
1576  print $fh "\n";
1577 }
1578 
1579 sub print_txns {
1580  my ($self, $fh, $id_map) = @_;
1581  print $fh "#### Transactions ####\n\n";
1582  my $txns = $self->get_txns();
1583  my $txn;
1584  foreach $txn (@$txns) {
1585  $txn->print($fh, '',$id_map);
1586  print $fh "\n";
1587  }
1588 }
1589 
1590 sub print {
1591  my($self, $fh) = @_;
1592 
1593  my %id_map;
1594  my $i = 0;
1595  my $accts = $self->get_accts();
1596  map {
1597  $id_map{$_} = "a$i";
1598  $i++;
1599  } @$accts;
1600  $i = 0;
1601  my $cats = $self->get_cats();
1602  map {
1603  $id_map{$_} = "c$i";
1604  $i++;
1605  } @$cats;
1606 
1607  print $fh "# CBB data file\n";
1608  print $fh "Version: 1.0\n";
1609  print $fh 'Default-sink: ' . $id_map{$self->get_default_sink()} . "\n";
1610  print $fh "\n";
1611 
1612  $self->print_sinks($fh, \%id_map);
1613  $self->print_txns($fh, \%id_map);
1614 }
1615 
1616 package CBBlib;
1617 
1618 sub key_colon_value_to_hash {
1619  my($text) = @_;
1620  # Assumes comment lines have already been stripped.
1621 
1622  my @lines = split("\n", $text);
1623  my %data;
1624 
1625  map {
1626  if($_ =~ m/\s*([^:]+):\s*(.*)$/o) {
1627  $data{$1} = $2;
1628  } else {
1629  die 'Bad line in database file, first "key: value" section.';
1630  }
1631  } @lines;
1632  return \%data;
1633 }
1634 
1635 sub load_file {
1636  # Args (filename:<string>)
1637 
1638  elapsed_reset("Starting load");
1639  my $name = shift;
1640  my $categories;
1641  my $accounts;
1642  my @transactions = ();
1643  my $fh = new IO::File;
1644  my $file;
1645 
1646  $fh->open($name) or die "Can't open input data file $file.";
1647  $fh->input_record_separator('');
1648 
1649  # Get the initial key/value pairs.
1650  my $text = <$fh>;
1651  $text =~ s/#.*//mgo; # Kill comment lines.
1652  $text =~ s/^\n//mgo; # Kill blank lines.
1653  my $file_data = key_colon_value_to_hash($text);
1654 
1655  die "Couldn't determine data file version"
1656  unless $$file_data{'Version'};
1657  die "Couldn't find default sink in $text."
1658  unless $$file_data{'Default-sink'};
1659 
1660  my %sink_map = ();
1661 
1662  # Get accounts
1663  $text = <$fh>;
1664  $text =~ s/#.*\n//mgo; # Kill comment lines.
1665  my @sinks = CBBlib::Db::extract_accounts_($text, \%sink_map);
1666 
1667  # Get categories
1668  $text = <$fh>;
1669  $text =~ s/#.*\n//mgo; # Kill comment lines.
1670  push @sinks, CBBlib::Db::extract_categories_($text, \%sink_map);
1671 
1672  my $default_sink = $sink_map{$$file_data{'Default-sink'}};
1673  die "Default sink " . $$file_data{'Default-sink'} .
1674  " not in database file." unless $default_sink;
1675 
1676  my $self = new CBBlib::Db($default_sink);
1677 
1678  $self->add_sinks(\@sinks);
1679 
1680 
1681  my @new_txns = ();
1682 
1683  # Read the "Transactions" comment line.
1684  scalar(<$fh>);
1685 
1686  my $count = 1;
1687  while(<$fh>) {
1688  print "\rLoading record $count";
1689  $count++;
1690 
1691  my @lines = split('\n', $_);
1692  my $transaction_info = shift @lines;
1693  my($date, $source, $checknum, $description, $status) =
1694  split("\t", $transaction_info);
1695  $source = $sink_map{$source};
1696 
1697  if(!defined($status) || $status eq '') { $status = ' '; }
1698 
1699  my $transaction =
1700  new CBBlib::Txn($date, $source, $checknum, $description, $status);
1701 
1702  # @lines is now just the split lines
1703  my $split_line;
1704  foreach $split_line (@lines) {
1705  $split_line =~ s/^\s*//o;
1706  my($destination, $note, $debit, $credit, $status) =
1707  split("\t", $split_line);
1708  if($destination) {
1709  my $old_dest = $destination;
1710  $destination = $sink_map{$destination};
1711  if(!$destination) {
1712  die "No destination for key [$old_dest]\n";
1713  }
1714  } else {
1715  $destination = $self->get_default_sink();
1716  }
1717 
1718  $transaction->add_split(new CBBlib::Split($destination, $note,
1719  $debit, $credit, $status));
1720  }
1721  push @new_txns, $transaction;
1722  }
1723  print "\n";
1724  elapsed();
1725  elapsed_reset(" Now adding transactions");
1726  $self->add_txns(\@new_txns);
1727  elapsed();
1728  elapsed_reset(" Calculating totals");
1729  $self->calc_account_totals_only_();
1730  elapsed();
1731  $fh->close() or die "Can't open close file $name.";
1732  print STDERR "finished load\n";
1733 
1734  return $self;
1735 }
1736 
1737 1;
1738 __END__
1739 ## @endcond Perl
Definition: SplitP.h:71