|
GnuCash 2.4.99
|
00001 #!/usr/bin/perl -w 00002 #use Exporter(); 00003 00004 package CBBlib; 00005 00006 use strict; 00007 use English; 00008 use IO; 00009 ## @file 00010 # @brief Belongs to package CBBlib 00011 # 00012 #### To do ###################### 00013 # 00014 # Check remove transactions. 00015 # Move everything to Cbb package. 00016 # 00017 # put warnings into add_txns and remove_txns if attempted when inside 00018 # begin/end_txn_modifications 00019 # 00020 # Check to see that set_db is OK in the face of modifications... 00021 # 00022 # update_ledger has to return a sorted list of txn indices for modified txns 00023 # 00024 # ledger_add/modify/remove_txns? 00025 # 00026 # note_txn_modification 00027 # 00028 # Dirty should be set whenever a modification is made. 00029 # Dirty should be cleared whenever the client tells us we're clear... 00030 # 00031 # What about clones and begin/end_modify_txns and no db? 00032 # 00033 # check copy_obj, and automate? 00034 # 00035 # Need to clone account balances too? 00036 # 00037 # Should have list positions in txns? 00038 # 00039 # Need to create "Unitemized" category by default. 00040 # 00041 # Don't sort modifications by serial number. Order is irrelevant. 00042 # Just use dates 00043 # @cond Perl 00044 00045 STDOUT->autoflush(1); 00046 STDERR->autoflush(1); 00047 00048 #@ISA = qw(Exporter); 00049 #@EXPORT = qw(func1 func2); 00050 #@EXPORT_OK = qw($sally @listabob %harry func3); 00051 00052 ## private ################################################################# 00053 00054 sub BEGIN {} 00055 00056 sub END { 00057 my $old_status = $?; 00058 00059 $? = $old_status; 00060 } 00061 00062 ## public ################################################################## 00063 00064 my $pref_debug = 0; 00065 my $pref_verbose = 0; 00066 00067 sub set_debug { 00068 my($value) = @_; 00069 $pref_debug = 1; 00070 } 00071 00072 sub set_verbose { 00073 my($value) = @_; 00074 $pref_verbose = 1; 00075 } 00076 00077 sub debug { 00078 my($message) = @_; 00079 print STDERR $message if $pref_debug; 00080 } 00081 00082 sub verbose { 00083 my($message) = @_; 00084 print STDERR $message if ($pref_verbose || $pref_debug); 00085 } 00086 00087 my $elapsed_offset = 0; 00088 00089 sub elapsed_reset { 00090 my $prefix = shift; 00091 my($user,$system) = times; 00092 $elapsed_offset = $user + $system; 00093 print STDERR $prefix . " ... " if $prefix; 00094 } 00095 sub elapsed { 00096 my $prefix = shift; 00097 print STDERR $prefix if $prefix; 00098 my($user,$system) = times; 00099 print STDERR '(elapsed time: ' . ($user + $system - $elapsed_offset) . ")\n"; 00100 } 00101 00102 package CBBlib::Sink; 00103 use strict; 00104 use English; 00105 use IO; 00106 00107 # Abstract class -- no "new" defined. 00108 00109 # A destination for money. Parent class for Acct and Cat. 00110 00111 # It's really possible that ledgers should be a separate class, but 00112 # I'm tired and that's too big a change right now. 00113 00114 00115 sub acquire_ledger { 00116 my $self = shift; 00117 my $ledger = $self->get_ledger_(); 00118 if(!$ledger) { 00119 $self->build_ledger_(); 00120 print STDERR "Building ledger\n"; 00121 } 00122 $self->set_ledger_usage_count_($self->get_ledger_usage_count_() + 1); 00123 return $self->get_ledger_(); 00124 } 00125 00126 sub release_ledger { 00127 my $self = shift; 00128 my $ledger = $self->get_ledger_(); 00129 if(!$ledger) { 00130 print STERR "Big problem. Released ledger that didn't exist!\n"; 00131 exit 1; 00132 } 00133 00134 my $usage_count = $self->get_ledger_usage_count_(); 00135 if(!$usage_count) { 00136 print STERR 00137 "Big problem. Released ledger that no-one could be holding!\n"; 00138 exit 1; 00139 } 00140 00141 $self->set_ledger_usage_count_($usage_count - 1); 00142 if($usage_count == 1) { 00143 print STDERR "Killing ledger\n"; 00144 $self->set_ledger_(undef); 00145 } 00146 } 00147 00148 sub build_ledger_ { 00149 my($self) = @_; 00150 my $db = $self->get_db(); 00151 my $transactions = $db->get_txns(); 00152 my $ledger = $self->get_ledger_(); 00153 if(!$ledger) { 00154 $self->set_ledger_([]); 00155 $ledger = $self->get_ledger_(); 00156 } 00157 @$ledger = (); 00158 00159 my $cleared_balance = 0; 00160 my $total = 0; 00161 my $txn; 00162 foreach $txn (@$transactions) { 00163 my($debit, $credit, $applicable_txn) = $txn->totals_wrt($self); 00164 if($applicable_txn) { 00165 $total += ($credit - $debit); 00166 if($txn->cleared_wrt_p($self)) { 00167 $cleared_balance += ($credit - $debit); 00168 } 00169 push @$ledger, [ $txn, $total ]; 00170 } 00171 } 00172 $self->set_cleared_balance_($cleared_balance); 00173 $self->set_final_balance_($total); 00174 } 00175 00176 sub compare_ledger_and_txn_ { 00177 my($ledger, $txn) = @_; 00178 return($$ledger[0]->get_date() cmp $txn->get_date()); 00179 } 00180 00181 sub match_ledger_and_txn_ { 00182 my($ledger, $txn) = @_; 00183 return($$ledger[0] == $txn); 00184 } 00185 00186 my $started_mods_p; 00187 my $mods_acct; 00188 my $cleared_balance_diff_tmp; 00189 00190 sub handle_ledger_entry_merge_ { 00191 my($ledger, $index, $new_p) = @_; 00192 00193 if($new_p || $started_mods_p) { 00194 if($new_p) { 00195 # convert txn to ledger pair. 00196 my $txn = $$ledger[$index]; 00197 $$ledger[$index] = [$txn, 0.0]; 00198 $started_mods_p = 1; 00199 } 00200 00201 my $entry = $$ledger[$index]; 00202 my $txn = $$entry[0]; 00203 my($debit, $credit, $applicable_txn) = $txn->totals_wrt($mods_acct); 00204 00205 if($new_p) { 00206 if($txn->cleared_wrt_p($mods_acct)) { 00207 $cleared_balance_diff_tmp += ($credit - $debit); 00208 } 00209 } 00210 00211 my $prev_total = 0; 00212 my $prev_entry = $$ledger[$index - 1]; 00213 if($prev_entry) { 00214 $prev_total = $$prev_entry[1]; 00215 } 00216 $$entry[1] = $prev_total + ($credit - $debit); 00217 } 00218 } 00219 00220 my $ledger_removal_diff; 00221 00222 sub handle_ledger_entry_removal_ { 00223 my($killed_p, $ledger, $index, $old_item) = @_; 00224 00225 if($killed_p || $started_mods_p) { 00226 my $txn = $$ledger[$index]; $txn = $$txn[0]; 00227 if($killed_p) { 00228 my($debit, $credit, $applicable_txn) = $txn->totals_wrt($mods_acct); 00229 $ledger_removal_diff += ($credit - $debit); 00230 if($txn->cleared_wrt_p($mods_acct)) { 00231 $cleared_balance_diff_tmp -= ($credit - $debit); 00232 } 00233 $started_mods_p = 1; 00234 } else { 00235 my $prev_total = 0; 00236 my $prev_entry = $$ledger[$index - 1]; 00237 if($prev_entry) { $prev_total = $$prev_entry[1]; } 00238 my $entry = $$ledger[$index]; 00239 $$entry[1] = $prev_total - $ledger_removal_diff; 00240 } 00241 } 00242 } 00243 00244 sub ledger_add_txns_ { 00245 my($self, $txns) = @_; 00246 00247 my $ledger = $self->get_ledger_(); 00248 if(!$ledger) { 00249 return []; 00250 } 00251 00252 # Globals, yuck... 00253 $cleared_balance_diff_tmp = 0; 00254 $started_mods_p = 0; 00255 $mods_acct = $self; 00256 CBBlib::debug("There are " . scalar(@$ledger) . " ledger entries\n"); 00257 CBBlib::debug(" adding " . scalar(@$txns) . " ledger entries\n"); 00258 $$txns[0]->print(\*STDERR); 00259 my $added_indices = 00260 main::destructive_merge_mangle($ledger, $txns, 00261 \&compare_ledger_and_txn_, 00262 \&handle_ledger_entry_merge_); 00263 $self->set_cleared_balance_($self->get_cleared_balance() + 00264 $cleared_balance_diff_tmp); 00265 my $final = $$ledger[$#$ledger]; 00266 $self->set_final_balance_($$final[1]); 00267 return $added_indices; 00268 } 00269 00270 sub ledger_modify_txns_ { 00271 my($self, $txn_date_changed, $txns) = @_; 00272 # presumes @$txns is sorted on date to match the database order. 00273 00274 my $ledger = $self->get_ledger_(); 00275 if(!$ledger) { 00276 return ([], []); 00277 } 00278 00279 # Brute force date change handling. We should do better later... 00280 my @txns_w_date_changes = grep { 00281 $$txn_date_changed{$_}; 00282 } @$txns; 00283 00284 my @moves = (); 00285 00286 if(@txns_w_date_changes) { 00287 print STDERR "Handling ledger mods for date changes\n"; 00288 00289 # Get the current positions of the changed $txns 00290 my %move_from_txn; 00291 my @candidates = @txns_w_date_changes; 00292 my $candidate = shift @candidates; 00293 # Unfortunately, these txns won't be in the order relative to the 00294 # old ledger since their dates have changed, so we have to loop. 00295 # This may be marginally faster than just traversing once per txn. 00296 while($candidate) { 00297 print STDERR "Looking for candidate $candidate\n"; 00298 my $initial_value = $candidate; 00299 my $i; 00300 for($i=0; $candidate && $i < scalar(@$ledger); $i++) { 00301 my $entry = $$ledger[$i]; 00302 my $txn = $$entry[0]; 00303 print STDERR "[$txn] [$candidate]\n"; 00304 if($txn == $candidate) { 00305 my $move = [$i]; 00306 push @moves, $move; 00307 $move_from_txn{$txn} = $move; 00308 $candidate = shift @candidates; 00309 } 00310 } 00311 if($initial_value == $candidate) { 00312 die "Couldn't find transaction in ledger during ledger modify"; 00313 } 00314 } 00315 00316 # Just trash the existing one to get the new one with these 00317 # transactions in the proper positions. 00318 $self->build_ledger_(); 00319 00320 # Now things should be properly sorted to match the order of 00321 # @txns_w_date_changes. We'll leave the extra loop just in case, 00322 # but this should go off in one pass. 00323 @candidates = @txns_w_date_changes; 00324 $candidate = shift @candidates; 00325 while($candidate) { 00326 my $initial_value = $candidate; 00327 my $i; 00328 for($i=0; $candidate && $i < scalar(@$ledger); $i++) { 00329 my $entry = $$ledger[$i]; 00330 my $txn = $$entry[0]; 00331 if($txn == $candidate) { 00332 my $move = $move_from_txn{$txn}; 00333 die "FATAL: Couldn't find move in ledger search" if ! $move; 00334 push @$move, $i; 00335 $candidate = shift @candidates; 00336 } 00337 } 00338 if($initial_value == $candidate) { 00339 die "Couldn't find transaction in ledger during ledger modify"; 00340 } 00341 } 00342 } 00343 00344 my @modified_indices = (); 00345 my $current_index = 0; 00346 00347 if(@$txns) { 00348 my @mod_txns = @$txns; 00349 my $next_mod_txn = shift @mod_txns; 00350 my $prev_ledger_value = 0; 00351 my $started_mods = 0; 00352 my $cleared_balance_diff = 0; 00353 my $diff = 0; 00354 my $entry; 00355 foreach $entry (@$ledger) { 00356 my $txn = $$entry[0]; 00357 if(defined($next_mod_txn) && $next_mod_txn == $txn) { 00358 $started_mods = 1; 00359 my $prev_value = $$entry[1]; 00360 my($debit, $credit, $applicable_txn) = $txn->totals_wrt($self); 00361 my $new_value = $prev_ledger_value + ($credit - $debit); 00362 my $local_change = $new_value - $prev_value; 00363 if($txn->cleared_wrt_p($self)) { 00364 $cleared_balance_diff -= ($credit - $debit); 00365 } 00366 $diff += $local_change; 00367 $$entry[1] = $new_value; 00368 $next_mod_txn = shift @mod_txns; 00369 push @modified_indices, $current_index; 00370 } elsif ($started_mods) { 00371 $$entry[1] += $diff; 00372 } 00373 $prev_ledger_value = $$entry[1]; 00374 $current_index++; 00375 } 00376 $self->set_cleared_balance_($self->get_cleared_balance() + 00377 $cleared_balance_diff); 00378 } 00379 my $final = $$ledger[$#$ledger]; 00380 $self->set_final_balance_($$final[1]); 00381 return (\@modified_indices, \@moves); 00382 } 00383 00384 sub ledger_remove_txns_ { 00385 my($self, $txns) = @_; 00386 # presumes @$txns is sorted on date to match the database order. 00387 # returns a list where each item is [$txn, $old_index]. 00388 00389 my $ledger = $self->get_ledger_(); 00390 if(!$ledger) { 00391 return; 00392 } 00393 00394 # Globals, yuck... 00395 $ledger_removal_diff = 0; 00396 $cleared_balance_diff_tmp = 0; 00397 $started_mods_p = 0; 00398 $mods_acct = $self; 00399 00400 my $removed_indices = 00401 main::destructive_remove_mangle($ledger, 00402 $txns, 00403 \&match_ledger_and_txn_, 00404 \&handle_ledger_entry_removal_); 00405 00406 print STDERR 00407 "Finished removing ledger items (" . join(" ", @$removed_indices) . 00408 ")\n"; 00409 00410 $self->set_cleared_balance_($self->get_cleared_balance() + 00411 $cleared_balance_diff_tmp); 00412 my $final = $$ledger[$#$ledger]; 00413 $self->set_final_balance_($$final[1]); 00414 00415 my @txns_tmp = @$txns; 00416 my @result = map { 00417 my $txn = shift @txns_tmp; 00418 [$_, $txn]; 00419 } @$removed_indices; 00420 return(\@result); 00421 } 00422 00423 00424 package CBBlib::Acct; 00425 use strict; 00426 use English; 00427 use IO; 00428 00429 use vars qw(@ISA); 00430 unshift @ISA, qw(CBBlib::Sink); 00431 00432 sub new { 00433 my $class = shift; 00434 my ($db, $name, $notes) = @_; 00435 my $self = make_internals_(); 00436 bless $self, $class; 00437 00438 $self->set_db_($db); 00439 $self->set_name_($name); 00440 $self->set_notes_($notes); 00441 00442 return $self; 00443 } 00444 00445 sub print { 00446 my($self, $fh, $prefix, $id_map) = @_; 00447 my $name = $self->get_name(); 00448 my $notes = $self->get_notes(); 00449 $notes = "" if ! $notes; 00450 $prefix = "" unless $prefix; 00451 if($id_map) { 00452 print $fh $prefix . $$id_map{$self} . "\t$name\t$notes\n"; 00453 } else { 00454 print $fh $prefix . "$self\t$name\t$notes\n"; 00455 } 00456 } 00457 00458 00459 package CBBlib::Cat; 00460 use strict; 00461 use English; 00462 use IO; 00463 00464 use vars qw(@ISA); 00465 unshift @ISA, qw(CBBlib::Sink); 00466 00467 sub new { 00468 my $class = shift; 00469 my ($db, $name, $notes) = @_; 00470 my $self = make_internals_(); 00471 bless $self, $class; 00472 00473 $self->set_db_($db); 00474 $self->set_name_($name); 00475 $self->set_notes_($notes); 00476 00477 return $self; 00478 } 00479 00480 sub print { 00481 my($self, $fh, $prefix, $id_map) = @_; 00482 my $name = $self->get_name(); 00483 my $notes = $self->get_notes(); 00484 $notes = "" if ! $notes; 00485 $prefix = "" unless $prefix; 00486 if($id_map) { 00487 print $fh $prefix . $$id_map{$self} . "\t$name\t$notes\n"; 00488 } else { 00489 print $fh $prefix . "$self\t$name\t$notes\n"; 00490 } 00491 } 00492 00493 package CBBlib::Txn; 00494 use strict; 00495 use English; 00496 use IO; 00497 00498 sub new { 00499 my $class = shift; 00500 my ($date, $source, $checkno, $desc, $status) = @_; 00501 $status = "" if !$status; 00502 00503 my $self = make_internals_(); 00504 bless $self, $class; 00505 00506 $self->set_date_($date); 00507 00508 die "CBBlib::Txn new: source must be a CBBlib::Acct." 00509 unless (ref($source) eq 'CBBlib::Acct'); 00510 00511 $self->set_source_($source); 00512 00513 $self->set_checkno_($checkno); 00514 $self->set_desc_($desc); 00515 $self->set_status_($status); 00516 00517 return $self; 00518 } 00519 00520 00521 sub make_clone_ { 00522 my ($self) = @_; 00523 00524 my $clone = $self->get_clone_(); 00525 if(!$clone) { 00526 $self->set_clone_($self->copy_obj_()); 00527 $clone = $self->get_clone_(); 00528 } 00529 return $clone; 00530 } 00531 00532 00533 00534 00535 # All the set/get slot functions are in CBBlib-auto.pl. 00536 00537 # "" = irrelevant (for split line with category destination) 00538 # " " = new and untouched 00539 # "*" = selected from the balance window to tentatively be 00540 # cleared (stage one of the balance process) 00541 # "x" = cleared (stage two of the balance process) 00542 # 00543 # "?" = a tentative future (recurring) transaction 00544 # "!" = a past (recurring) transaction 00545 00546 sub get_status_wrt { 00547 my($self, $sink) = @_; 00548 my $result; 00549 if($self->get_source() == $sink) { 00550 $result = $self->get_status(); 00551 } else { 00552 $result = $self->get_transfer_status($sink); 00553 } 00554 if(!defined($result)) { 00555 print STDERR "Undefined status wrt $sink for\n"; 00556 $self->print(\*STDERR, " "); 00557 } 00558 return $result; 00559 } 00560 00561 sub set_status_wrt_ { 00562 my($self, $sink, $val) = @_; 00563 if($self->get_source() == $sink) { 00564 $self->set_status($val); 00565 } else { 00566 $self->set_transfer_status_($sink, $val); 00567 } 00568 } 00569 00570 sub copy_obj_ { 00571 my($self) = shift; 00572 00573 my @copy = @$self; 00574 my $copy_ref = \@copy; 00575 bless $copy_ref, ref($self); 00576 00577 my $splits = $copy_ref->get_splits_(); 00578 00579 # Copy only the spine of the list (so we can do a "diff" later). 00580 my @splits_copy = @$splits; 00581 00582 $copy_ref->set_splits_(\@splits_copy); 00583 return $copy_ref; 00584 } 00585 00586 00587 sub get_transfer_status { 00588 my($self, $sink) = @_; 00589 my $splits = $self->get_splits_(); 00590 my $result; 00591 my $split; 00592 foreach $split (@$splits) { 00593 if($split->get_dest() == $sink) { 00594 $result = $split; 00595 last; 00596 } 00597 } 00598 undef $split; 00599 00600 if(!$result) { 00601 return undef; 00602 } else { 00603 return $result->get_status(); 00604 } 00605 } 00606 00607 00608 sub set_transfer_status_ { 00609 my($self, $acct, $new_status) = @_; 00610 my $splits = $self->get_splits_(); 00611 my $result; 00612 my $split; 00613 foreach $split (@$splits) { 00614 my $dest = $split->get_dest(); 00615 if($split->get_dest() == $acct) { 00616 $split->set_status($new_status); 00617 } 00618 } 00619 } 00620 00621 sub cleared_p_ { 00622 my($self) = @_; 00623 return $self->get_status() eq 'x'; 00624 } 00625 00626 sub cleared_wrt_p { 00627 my($self, $acct) = @_; 00628 return $self->get_status_wrt($acct) eq 'x'; 00629 } 00630 00631 sub clear_wrt { 00632 my($self, $acct) = @_; 00633 $self->set_status_wrt_($acct, 'x'); 00634 } 00635 00636 sub clear_pending_wrt_p { 00637 my($self, $acct) = @_; 00638 return $self->get_status_wrt($acct) eq '*'; 00639 } 00640 00641 sub clear_pending_wrt { 00642 my($self, $acct) = @_; 00643 $self->set_status_wrt_($acct, '*'); 00644 } 00645 00646 sub uncleared_wrt_p { 00647 my($self, $acct) = @_; 00648 return $self->get_status_wrt($acct) eq ' '; 00649 } 00650 00651 sub unclear_wrt { 00652 my($self, $acct) = @_; 00653 $self->set_status_wrt_($acct, ' '); 00654 } 00655 00656 sub void { 00657 # ??? 00658 } 00659 00660 sub add_split { 00661 my($self, $split, $insert_position) = @_; 00662 00663 # Don't forget to change modify functions in CBBlib-auto.plp too 00664 # whenever you make changes here. 00665 00666 my $splits; 00667 00668 $split->set_txn_($self); 00669 00670 my $db = $self->get_db(); 00671 if($db) { 00672 00673 $db->begin_txn_modifications(); 00674 00675 my $clone = $self->make_clone_(); 00676 $splits = $clone->get_splits_(); 00677 00678 if($insert_position) { 00679 splice @$splits, $insert_position, 0, ($split); 00680 } else { 00681 push @$splits, $split; 00682 $insert_position = $#$splits; 00683 } 00684 } else { 00685 $splits = $self->get_splits_(); 00686 00687 if($insert_position) { 00688 splice @$splits, $insert_position, 0, ($split); 00689 } else { 00690 push @$splits, $split; 00691 $insert_position = $#$splits; 00692 } 00693 } 00694 00695 my $i; 00696 for($i = $insert_position; $i < scalar(@$splits); $i++) { 00697 $$splits[$i]->set_pos_($i); 00698 } 00699 00700 if($db) { 00701 $db->record_txn_modification_($self); 00702 $db->end_txn_modifications(); 00703 } 00704 } 00705 00706 sub remove_split { 00707 my($self, $split) = @_; 00708 00709 # Don't forget to change modify functions in CBBlib-auto.plp too 00710 # whenever you make changes here. 00711 00712 my $db = $self->get_db(); 00713 my $splits; 00714 if($db) { 00715 $db->begin_txn_modifications(); 00716 00717 my $clone = $self->make_clone_(); 00718 $splits = $clone->get_splits_(); 00719 } else { 00720 $splits = $self->get_splits_(); 00721 } 00722 00723 my $old_index = 0; 00724 my $split_found = 0; 00725 my $candidate; 00726 foreach $candidate (@$splits) { 00727 if($candidate == $split) { 00728 $split_found = 1; 00729 last; 00730 } 00731 $old_index++; 00732 } 00733 if(!$split_found) { 00734 die "Failed to find split in Txn::remove_split"; 00735 } 00736 splice @$splits, $old_index, 0; 00737 00738 my $i; 00739 for($i = $old_index; $i < scalar(@$splits); $i++) { 00740 $$splits[$i]->set_pos_($i); 00741 } 00742 00743 if($db) { 00744 my $txn = $split->get_txn_(); 00745 $split->set_txn_(undef); 00746 $db->record_txn_modification_($txn); 00747 $db->end_txn_modifications(); 00748 } 00749 } 00750 00751 sub totals_wrt { 00752 my($self, $sink) = @_; 00753 # O(n) 00754 # Returns (total_debit, total_credit, applicable) 00755 00756 my($total_debit, $total_credit, $applicability) = (0, 0, 0); 00757 my $splits = $self->get_splits_(); 00758 00759 my $split; 00760 foreach $split (@$splits) { 00761 my $dest = $split->get_dest(); 00762 00763 if($self->get_source() == $sink) { 00764 $total_debit += $split->get_debit(); 00765 $total_credit += $split->get_credit(); 00766 $applicability = 1; 00767 } elsif($dest == $sink) { 00768 #} elsif((ref($dest) eq 'CBBlib::Acct') && ($dest == $sink)) { 00769 $total_debit += $split->get_credit(); 00770 $total_credit += $split->get_debit(); 00771 $applicability = 1; 00772 } 00773 } 00774 return($total_debit, $total_credit, $applicability); 00775 } 00776 00777 00778 sub affected_sinks { 00779 my($self) = @_; 00780 # O(n) 00781 # Returns list of affected sinks 00782 00783 my $source = $self->get_source(); 00784 my %result = ($source => $source); 00785 my $splits = $self->get_splits_(); 00786 00787 map { 00788 my $dest = $_->get_dest(); 00789 if(!$dest) { 00790 die "\nNo dest in $_\n"; 00791 } 00792 $result{$dest} = $dest; 00793 } @$splits; 00794 return(values(%result)); 00795 } 00796 00797 00798 sub print { 00799 my($self, $fh, $prefix, $id_map) = @_; 00800 $prefix = "" if ! $prefix; 00801 00802 print $fh $prefix . $self->get_date() . "\t"; 00803 if($id_map) { 00804 print $fh $$id_map{$self->get_source()} . "\t"; 00805 } else { 00806 print $fh $self->get_source() . "\t"; 00807 } 00808 print $fh $self->get_checkno() . "\t"; 00809 print $fh $self->get_desc() . "\t"; 00810 print $fh $self->get_status() . "\n"; 00811 my $splits = $self->get_splits_(); 00812 my $split; 00813 foreach $split (@$splits) { 00814 $split->print($fh, $prefix . ' ', $id_map); 00815 } 00816 } 00817 00818 sub print_pretty { 00819 my($self, $fh, $prefix) = @_; 00820 $prefix = "" if ! $prefix; 00821 00822 print $fh $prefix . $self->get_date() . ":"; 00823 print $fh ($self->get_source())->get_name() . ":"; 00824 print $fh $self->get_checkno() . ":"; 00825 print $fh $self->get_desc() . ":"; 00826 print $fh $self->get_status() . "\n"; 00827 my $splits = $self->get_splits_(); 00828 my $split; 00829 foreach $split (@$splits) { 00830 $split->print_pretty($fh, $prefix . " "); 00831 } 00832 } 00833 00834 package CBBlib::Split; 00835 use strict; 00836 use English; 00837 use IO; 00838 00839 sub new { 00840 my $class = shift; 00841 my ($dest, $notes, $debit, $credit, $status) = @_; 00842 my $self = make_internals_(); 00843 bless $self, $class; 00844 00845 $status = '' if ! $status; 00846 00847 $self->set_dest_($dest); 00848 $self->set_notes_($notes); 00849 $self->set_debit_($debit); 00850 $self->set_credit_($credit); 00851 $self->set_status_($status); 00852 00853 return $self; 00854 } 00855 00856 sub copy_obj_ { 00857 my($self) = shift; 00858 00859 my @copy = @$self; 00860 my $copy_ref = \@copy; 00861 bless $copy_ref, ref($self); 00862 return $copy_ref; 00863 } 00864 00865 sub make_clone_ { 00866 my ($self) = @_; 00867 00868 my $clone = $self->get_clone_(); 00869 if(!$clone) { 00870 my $txn = $self->get_txn_(); 00871 if($txn) { 00872 $txn->make_clone_(); 00873 } 00874 $self->set_clone_($self->copy_obj_()); 00875 $clone = $self->get_clone_(); 00876 } 00877 return $clone; 00878 } 00879 00880 sub get_db { 00881 my $self = shift; 00882 my $txn = $self->get_txn(); 00883 if($txn) { 00884 return $txn->get_db(); 00885 } else { 00886 return undef; 00887 } 00888 } 00889 00890 sub cleared_p_ { 00891 my($self) = @_; 00892 my $status = $self->get_status(); 00893 if($status) { 00894 return $status eq 'x'; 00895 } else { 00896 return undef; 00897 } 00898 } 00899 00900 00901 sub print { 00902 my($self, $fh, $prefix, $id_map) = @_; 00903 $prefix = "" if ! $prefix; 00904 00905 print $fh $prefix; 00906 if($id_map) { 00907 print $fh $$id_map{$self->get_dest()} . "\t"; 00908 } else { 00909 print $fh $self->get_dest() . "\t"; 00910 } 00911 print $fh $self->get_notes() . "\t"; 00912 print $fh $self->get_debit() . "\t"; 00913 print $fh $self->get_credit() . "\t"; 00914 print $fh $self->get_status() . "\n"; 00915 } 00916 00917 00918 package CBBlib::Db; 00919 use strict; 00920 use English; 00921 use IO; 00922 00923 sub new { 00924 my($class, $default_sink) = @_; 00925 my $self = make_internals_(); 00926 bless $self, $class; 00927 00928 if($default_sink) { 00929 $self->add_sinks([$default_sink]); 00930 } else { 00931 $default_sink = new CBBlib::Cat($self, '<<unitemized>>', ''); 00932 $self->add_sinks([$default_sink]); 00933 } 00934 $self->set_default_sink($default_sink); 00935 00936 return $self; 00937 } 00938 00939 sub clean_p { 00940 # Should eventually know the truth, but this is safe at the moment. 00941 return 0; 00942 } 00943 00944 sub add_sinks { 00945 my($self, $sinks) = @_; 00946 00947 my $accts = $self->get_accts_(); 00948 my $cats = $self->get_cats_(); 00949 map { 00950 $_->set_db_($self); 00951 if(ref($_) eq 'CBBlib::Acct') { 00952 push @$accts, $_; 00953 } elsif(ref($_) eq 'CBBlib::Cat') { 00954 push @$cats, $_; 00955 } else { 00956 die "Unknown sink type in CBBlib::Db::add_sinks()"; 00957 } 00958 } @$sinks; 00959 } 00960 00961 sub record_txn_modification_ { 00962 my($self, $txn) = @_; 00963 00964 my $mod_level = $self->get_modified_txns_level_(); 00965 if(!$mod_level) { 00966 die "Tried to record_txn_modification_ when not in update region."; 00967 } 00968 my $modified_txns = $self->get_modified_txns_(); 00969 #my $serial_number = $self->get_modified_txns_serial_num_(); 00970 #$self->set_modified_txns_serial_num_($serial_number + 1); 00971 00972 if(ref($txn) eq 'CBBlib::Split') { 00973 $txn = $txn->get_txn_(); 00974 } 00975 $$modified_txns{$txn} = $txn; 00976 } 00977 00978 sub update_dirty_txns_hash_ { 00979 my($self, $dirty_hash, $txn) = @_; 00980 00981 my @affected_sinks = $txn->affected_sinks(); 00982 00983 map { 00984 my $sink = $_; 00985 if(!$$dirty_hash{$sink}) { 00986 $$dirty_hash{$sink} = [$sink, [$txn]]; 00987 } else { 00988 my $list = $$dirty_hash{$sink}; 00989 $list = $$list[1]; 00990 push @$list, $txn; 00991 } 00992 } @affected_sinks; 00993 } 00994 00995 00996 sub debug_txns_modified_data { 00997 my($self, $modifications, $dirty_sinks) = @_; 00998 CBBlib::debug("CBBlib CALLBACK: txns-modified\n"); 00999 01000 CBBlib::debug(" Modifications:\n"); 01001 map { 01002 my $txn = $$_[0]; 01003 my $mods = $$_[1]; 01004 CBBlib::debug(" [$txn"); 01005 map { 01006 CBBlib::debug("\n ["); 01007 my $first = 1; 01008 map { 01009 if($first) { 01010 $first = 0; 01011 } else { 01012 print " "; 01013 } 01014 if(!defined($_)) { 01015 print "<<undefined>>"; 01016 } else { 01017 print $_; 01018 } 01019 } @$_; 01020 CBBlib::debug("]"); 01021 } @$mods; 01022 CBBlib::debug("]\n"); 01023 } @$modifications; 01024 01025 CBBlib::debug(" Affected sinks:\n"); 01026 map { 01027 my ($sink, $mod_txns, $indices) = @$_; 01028 $sink->print(\*STDERR, " "); 01029 } values(%$dirty_sinks); 01030 } 01031 01032 01033 sub post_modification_notices_ { 01034 my $self = shift; 01035 CBBlib::debug("CBBlib::post_modification_notices_: checking for changes.\n"); 01036 01037 my $modified_txns_hash = $self->get_modified_txns_(); 01038 my @sorted_txns = sort { 01039 $a->get_date <=> $b->get_date(); 01040 } values(%$modified_txns_hash); 01041 my @modifications = (); 01042 01043 # Have to treat credit/debit mods the same as other field mods since 01044 # in the end we need the ledger indices of all the txns. 01045 my %dirty_sinks; 01046 01047 # This is not the most efficient way to go about this, but it's 01048 # easy, and I'm in a hurry. Someone can make it do the merge/remove 01049 # mangle thing later. Be careful, though, you don't want to cause 01050 # spurious add/remove events. 01051 my $date_changed; 01052 my %txn_date_changed; 01053 01054 # generate modified events for each transaction 01055 my $txn; 01056 foreach $txn (@sorted_txns) { 01057 my $new_txn = $txn->get_clone_(); 01058 my $old_splits = $txn->get_splits_(); 01059 my $new_splits = $new_txn->get_splits_(); 01060 my @txn_mods = (); 01061 01062 my $i; 01063 for($i=0; $i < scalar(@$old_splits); $i++) { 01064 my $old_item = $$old_splits[$i]; 01065 if(! grep { $_ == $old_item } @$new_splits) { 01066 my $old_index = $i; 01067 push @txn_mods, ['split-removed', $old_item, $old_index]; 01068 } 01069 } 01070 01071 for($i=0; $i < scalar(@$new_splits); $i++) { 01072 my $new_item = $$new_splits[$i]; 01073 if(! grep { $_ == $new_item } @$old_splits) { 01074 my $new_index = $i; 01075 push @txn_mods, ['split-added', $new_item, $new_index]; 01076 } 01077 } 01078 01079 # This could be much faster with a better algorithm. 01080 my @modified_splits = grep { $_->get_clone_() } @$new_splits; 01081 01082 if($new_txn->get_date_ ne $txn->get_date_()) { 01083 push @txn_mods, ['date', $txn->get_date_()]; 01084 $txn->set_date_($new_txn->get_date_()); 01085 $date_changed = 1; 01086 $txn_date_changed{$txn} = $txn; 01087 } 01088 if($new_txn->get_checkno_ ne $txn->get_checkno_()) { 01089 push @txn_mods, ['checkno', $txn->get_checkno_()]; 01090 $txn->set_checkno_($new_txn->get_checkno_()); 01091 } 01092 if($new_txn->get_desc_ ne $txn->get_desc_()) { 01093 push @txn_mods, ['desc', $txn->get_desc_()]; 01094 $txn->set_desc_($new_txn->get_desc_()); 01095 } 01096 if($new_txn->get_status_ ne $txn->get_status_()) { 01097 push @txn_mods, ['status', $txn->get_status_()]; 01098 $txn->set_status_($new_txn->get_status_()); 01099 } 01100 01101 my $split; 01102 foreach $split (@modified_splits) { 01103 my $new_split = $split->get_clone_(); 01104 01105 my $old_txn = $split->get_txn(); 01106 if(defined($old_txn) && ($old_txn == $new_split->get_txn())) { 01107 # This is really a modification to an existing split (is that 01108 # what this test should be doing)? 01109 01110 my $old_pos = $split->get_pos__(); 01111 my $new_pos = $new_split->get_pos__(); 01112 if((defined($new_pos) != defined($old_pos)) || 01113 ($new_pos != $old_pos)) { 01114 push @txn_mods, ['split-modified', $split, 'pos', 01115 $split->get_pos__()]; 01116 $split->set_pos__($new_split->get_pos__()); 01117 } 01118 if($new_split->get_dest_ != $split->get_dest_()) { 01119 push @txn_mods, ['split-modified', $split, 'dest', 01120 $split->get_dest_()]; 01121 $split->set_dest_($new_split->get_dest_()); 01122 } 01123 if($new_split->get_notes_ ne $split->get_notes_()) { 01124 push @txn_mods, ['split-modified', $split, 'notes', 01125 $split->get_notes_()]; 01126 $split->set_notes_($new_split->get_notes_()); 01127 } 01128 if($new_split->get_debit_ != $split->get_debit_()) { 01129 push @txn_mods, ['split-modified', $split, 'debit', 01130 $split->get_debit_()]; 01131 $split->set_debit_($new_split->get_debit_()); 01132 } 01133 if($new_split->get_credit_ != $split->get_credit_()) { 01134 push @txn_mods, ['split-modified', $split, 'credit', 01135 $split->get_credit_()]; 01136 $split->set_credit_($new_split->get_credit_()); 01137 } 01138 if($new_split->get_status_ ne $split->get_status_()) { 01139 push @txn_mods, ['split-modified', $split, 'status', 01140 $split->get_status_()]; 01141 $split->set_status_($new_split->get_status_()); 01142 } 01143 } 01144 $split->set_clone_(undef); 01145 } 01146 $txn->set_splits_($new_splits); 01147 $txn->set_clone_(undef); 01148 01149 if(@txn_mods) { 01150 $self->update_dirty_txns_hash_(\%dirty_sinks, $txn); 01151 push @modifications, [ $txn, \@txn_mods ]; 01152 } 01153 } 01154 01155 # Re-sort if needed, so the positions will be right for the next 01156 # steps. 01157 if($date_changed) { 01158 print STDERR "Re-sorting txns\n"; 01159 my $txns = $self->get_txns(); 01160 @$txns = sort { 01161 $a->get_date <=> $b->get_date(); 01162 } @$txns; 01163 } 01164 01165 # Act on dirty ledgers hash. 01166 my $acct_data; 01167 foreach $acct_data (values %dirty_sinks) { 01168 my $acct = $$acct_data[0]; 01169 my $acct_txns = $$acct_data[1]; 01170 my ($txn_indices, $moves) = 01171 $acct->ledger_modify_txns_(\%txn_date_changed, 01172 $acct_txns); 01173 push @$acct_data, $txn_indices, $moves; 01174 } 01175 01176 if(@modifications) { 01177 01178 debug_txns_modified_data($self, \@modifications, \%dirty_sinks); 01179 01180 # txns-modified callback 01181 my $callbacks_hash = $self->get_callbacks_hash_(); 01182 my $callbacks = $$callbacks_hash{'txns-modified'}; 01183 my $callback; 01184 foreach $callback (@$callbacks) { 01185 my $func = $$callback[0]; 01186 my $args = $$callback[1]; 01187 01188 # @modifications elements are of the form 01189 # [ $txn, [ mod, mod, mod, ...]] 01190 01191 # %dirty_sinks 01192 # Key: $sink 01193 # Value: [ $sink, @$txns, @$current_indices, @$moves] 01194 # where @$moves is a ref to a list of pairs 01195 # of the form [$prev_pos, $new_pos] sorted on $prev_pos 01196 01197 &$func($self, \@modifications, \%dirty_sinks, $args); 01198 01199 } 01200 } 01201 } 01202 01203 01204 sub begin_txn_modifications { 01205 my ($self) = shift; 01206 my $mod_level = $self->get_modified_txns_level_(); 01207 if(!$mod_level) { 01208 $self->set_modified_txns_({}); 01209 #$self->set_modified_txns_serial_num_(0); 01210 } 01211 $self->set_modified_txns_level_($mod_level + 1); 01212 } 01213 01214 sub end_txn_modifications { 01215 my ($self) = shift; 01216 my $mod_level = $self->get_modified_txns_level_(); 01217 if($mod_level == 1) { 01218 $self->post_modification_notices_(); 01219 } elsif(!$mod_level) { 01220 die 01221 "Big problem. Db::end_txn_modifications called when not modifying.\n"; 01222 } 01223 $self->set_modified_txns_level_($mod_level - 1); 01224 } 01225 01226 sub get_accts_by_name { 01227 my($self, $name) = @_; 01228 my $accts = $self->get_accts(); 01229 my @matches = grep { 01230 if($_) { 01231 $_->get_name() eq $name; 01232 } else { 01233 0; 01234 } 01235 } @$accts; 01236 return \@matches; 01237 } 01238 01239 sub get_cats_by_name { 01240 my($self, $name) = @_; 01241 my $cats = $self->get_cats(); 01242 my @matches = grep { 01243 if($_) { 01244 $_->get_name() eq $name; 01245 } else { 01246 0; 01247 } 01248 } @$cats; 01249 return \@matches; 01250 } 01251 01252 sub extract_accounts_ { 01253 my($text, $hash) = @_; 01254 my @accounts = split("\n", $text); 01255 return map { 01256 my $acct = $_; 01257 my @fields = split("\t", $acct); 01258 (scalar(@fields) < 4) or die "Wrong number of fields in account."; 01259 01260 my $name = $fields[1]; 01261 my $notes = $fields[2]; 01262 $acct = new CBBlib::Acct(undef, $name, $notes); 01263 $$hash{$fields[0]} = $acct; 01264 $acct; 01265 } @accounts; 01266 } 01267 01268 sub extract_categories_ { 01269 my($text, $hash) = @_; 01270 my @categories = split("\n", $text); 01271 return map { 01272 my $cat = $_; 01273 my @fields = split("\t", $cat); 01274 (scalar(@fields) < 4) or die "Wrong number of fields in category."; 01275 01276 my $name = $fields[1]; 01277 my $notes = $fields[2]; 01278 $cat = new CBBlib::Cat(undef, $name, $notes); 01279 $$hash{$fields[0]} = $cat; 01280 $cat; 01281 } @categories; 01282 } 01283 01284 sub calc_account_totals_only_ { 01285 my($self) = @_; 01286 my $transactions = $self->get_txns(); 01287 my $accts = $self->get_accts(); 01288 map { 01289 if($_) { 01290 $_->set_cleared_balance_(0); 01291 $_->set_final_balance_(0); 01292 } 01293 } @$accts; 01294 01295 #my $cleared_balance = 0; 01296 #my $final_balance = 0; 01297 01298 my $txn; 01299 foreach $txn (@$transactions) { 01300 my $splits = $txn->get_splits_(); 01301 01302 my $split; 01303 foreach $split (@$splits) { 01304 01305 my $source = $txn->get_source(); 01306 my $dest = $split->get_dest(); 01307 my $debit = $split->get_debit(); 01308 my $credit = $split->get_credit(); 01309 01310 my $cleared_bal; 01311 my $final_bal; 01312 my $diff = $credit - $debit; 01313 01314 if($txn->cleared_p_()) { 01315 $cleared_bal = $source->get_cleared_balance() + $diff; 01316 $source->set_cleared_balance_($cleared_bal); 01317 } 01318 $final_bal = $source->get_final_balance() + $diff; 01319 $source->set_final_balance_($final_bal); 01320 01321 if(ref($dest) eq 'CBBlib::Acct' && ($source != $dest)) { 01322 if($split->cleared_p_()) { 01323 $cleared_bal = $dest->get_cleared_balance(); 01324 $dest->set_cleared_balance_($cleared_bal - $diff); 01325 } 01326 $final_bal = $dest->get_final_balance() - $diff; 01327 $dest->set_final_balance_($final_bal); 01328 } 01329 } 01330 } 01331 } 01332 01333 01334 sub add_callback_ { 01335 my($self, $name, $callback, $user_data) = @_; 01336 my $data = [$callback, $user_data]; 01337 my $callbacks_hash = $self->get_callbacks_hash_(); 01338 my $txn_callbacks = $$callbacks_hash{$name}; 01339 if(!$txn_callbacks) { 01340 $$callbacks_hash{$name} = []; 01341 $txn_callbacks = $$callbacks_hash{$name}; 01342 } 01343 push @$txn_callbacks, $data; 01344 return $data; 01345 } 01346 01347 sub remove_callback_ { 01348 my($self, $name, $callback_id) = @_; 01349 my $callbacks_hash = $self->get_callbacks_hash_(); 01350 my $callbacks = $$callbacks_hash{$name}; 01351 if(scalar(@$callbacks)) { 01352 @$callbacks = grep { !($_ == $callback_id) } @$callbacks; 01353 } 01354 } 01355 01356 # add_txn_callback 01357 # Called whenever new transactions are added 01358 # Called with args ($db, $new_txns, $user_data) 01359 01360 sub add_txns_added_callback { 01361 my($self, $callback, $user_data) = @_; 01362 return $self->add_callback_('txns-added', $callback, $user_data); 01363 } 01364 01365 01366 sub remove_txns_added_callback { 01367 my($self, $callback_id) = @_; 01368 $self->remove_callback_('txns-added', $callback_id); 01369 } 01370 01371 01372 # add_txn_callback 01373 # Called whenever new transactions are added 01374 # Called with args ($db, $dead_txns, $user_data) 01375 01376 sub add_txns_removed_callback { 01377 my($self, $callback, $user_data) = @_; 01378 return $self->add_callback_('txns-removed', $callback, $user_data); 01379 } 01380 01381 01382 sub remove_txns_removed_callback { 01383 my($self, $callback_id) = @_; 01384 $self->remove_callback_('txns-removed', $callback_id); 01385 } 01386 01387 sub add_txns_modified_callback { 01388 my($self, $callback, $user_data) = @_; 01389 return $self->add_callback_('txns-modified', $callback, $user_data); 01390 } 01391 01392 sub remove_txns_modified_callback { 01393 my($self, $callback_id) = @_; 01394 $self->remove_callback_('txns-modified', $callback_id); 01395 } 01396 01397 sub merge_new_txns_into_main_list_ { 01398 my($self, $new_txns) = @_; 01399 my $txns = $self->get_txns(); 01400 01401 my $added_indices = 01402 main::destructive_merge_mangle($txns, $new_txns, sub { 01403 return $_[0]->get_date() cmp $_[1]->get_date(); 01404 }); 01405 01406 map { $_->set_db_($self); } @$new_txns; 01407 01408 my %affected_sinks; 01409 my $txn; 01410 foreach $txn (@$new_txns) { 01411 my @accts = $txn->affected_sinks(); 01412 my $acct; 01413 foreach $acct (@accts) { 01414 my $data = $affected_sinks{$acct}; 01415 if(!$data) { $data = $affected_sinks{$acct} = [$acct, []]; } 01416 my $list = $$data[1]; 01417 push @$list, $txn; 01418 } 01419 } 01420 01421 return (\%affected_sinks, $added_indices); 01422 } 01423 01424 sub merge_new_txns_into_ledger_lists_ { 01425 my($self, $new_txns, $affected_accts) = @_; 01426 01427 # $affected accts is a hash mapping accounts to [acct, 01428 # relevant_txns] acct is a ref to the account, and relevant txns is 01429 # a ref to a list of the relevant transactions. The transactions 01430 # must be ordered in each list like they are in the global DB. 01431 01432 # We're going to add the resulting new ledger indices to the hash 01433 # values so we have: [acct, relevant_txns, indices] 01434 01435 my $data; 01436 foreach $data (values(%$affected_accts)) { 01437 my $acct_ref = $$data[0]; 01438 my $txns = $$data[1]; 01439 my $added_indices = $acct_ref->ledger_add_txns_($txns); 01440 push @$data, $added_indices; 01441 } 01442 } 01443 01444 sub add_txns { 01445 my($self, $new_txns) = @_; 01446 01447 @$new_txns = sort { 01448 $a->get_date() <=> $b->get_date(); 01449 } @$new_txns; 01450 01451 my ($affected_accts, $added_indices) = 01452 $self->merge_new_txns_into_main_list_($new_txns); 01453 01454 $self->merge_new_txns_into_ledger_lists_($new_txns, $affected_accts); 01455 01456 my $callbacks_hash = $self->get_callbacks_hash_(); 01457 my $txn_callbacks = $$callbacks_hash{'txns-added'}; 01458 my $callback; 01459 foreach $callback (@$txn_callbacks) { 01460 my $func = $$callback[0]; 01461 my $args = $$callback[1]; 01462 01463 if($main::pref_debug) { 01464 print STDERR 01465 "(txns-added\n" . 01466 " db: $self\n" . 01467 ' added-indices: (' . join("\n" . 01468 ' ', @$added_indices) . ")\n" . 01469 " (affected-accts\n"; 01470 my $acct_data; 01471 foreach $acct_data (values(%$affected_accts)) { 01472 my $acct = $$acct_data[0]; 01473 my $txns = $$acct_data[1]; 01474 my $indices = $$acct_data[2]; 01475 print STDERR 01476 " acct: $acct\n" . 01477 ' txns: (' . join("\n" . 01478 ' ', @$txns) . ")\n" . 01479 ' indices: (' . join("\n" . 01480 ' ', @$indices) . ")\n"; 01481 } 01482 CBBlib::debug(' args: ' . $args . "))\n"); 01483 } 01484 &$func($self, $added_indices, $affected_accts, $args); 01485 } 01486 } 01487 01488 sub remove_txns_from_ledger_lists_ { 01489 my($self, $dead_txns, $affected_accts) = @_; 01490 # $affected accts is a hash mapping accounts to refs to lists 01491 # of relevant transactions. The transactions must be ordered 01492 # in each list like they are in the global DB. 01493 01494 # returns a hash from $sink to a listref of [$sink, @$txn_info] where 01495 # @$txn_info is a lists of [$txn, $prev_ledger_index] pairs 01496 01497 my %result; 01498 01499 my $acct; 01500 foreach $acct (keys(%$affected_accts)) { 01501 my $data = $$affected_accts{$acct}; 01502 my $acct_ref = $$data[0]; 01503 my $txns = $$data[1]; 01504 my $removal_info = $acct_ref->ledger_remove_txns_($txns); 01505 $result{$acct} = [$acct, $removal_info]; 01506 } 01507 return \%result; 01508 } 01509 01510 sub remove_txns_from_main_list_ { 01511 my($self, $dead_txns) = @_; 01512 my $txns = $self->get_txns(); 01513 01514 my $removed_indices = 01515 main::destructive_remove_mangle($txns, $dead_txns, sub { 01516 return $_[0] == $_[1]; 01517 }); 01518 01519 map { $_->set_db_($self); } @$dead_txns; 01520 01521 my %affected_accts; 01522 my $txn; 01523 foreach $txn (@$dead_txns) { 01524 my @accts = $txn->affected_sinks(); 01525 my $acct; 01526 foreach $acct (@accts) { 01527 my $data = $affected_accts{$acct}; 01528 if(!$data) { $data = $affected_accts{$acct} = [$acct, []]; } 01529 my $list = $$data[1]; 01530 push @$list, $txn; 01531 } 01532 } 01533 01534 return (\%affected_accts, $removed_indices); 01535 } 01536 01537 sub remove_txns { 01538 my($self, $dead_txns) = @_; 01539 01540 @$dead_txns = sort { 01541 $a->get_date() cmp $b->get_date(); 01542 } @$dead_txns; 01543 01544 my ($affected_accts, $removed_db_indices) = 01545 $self->remove_txns_from_main_list_($dead_txns); 01546 01547 my $ledger_removal_info = 01548 $self->remove_txns_from_ledger_lists_($dead_txns, $affected_accts); 01549 01550 my $callbacks_hash = $self->get_callbacks_hash_(); 01551 my $txn_callbacks = $$callbacks_hash{'txns-removed'}; 01552 my $callback; 01553 foreach $callback (@$txn_callbacks) { 01554 my $func = $$callback[0]; 01555 my $args = $$callback[1]; 01556 &$func($self, $dead_txns, 01557 $removed_db_indices, 01558 $ledger_removal_info, 01559 $args); 01560 } 01561 } 01562 01563 sub print_sinks { 01564 my ($self, $fh, $id_map) = @_; 01565 01566 print $fh "#### Accounts ####\n"; 01567 my $accts = $self->get_accts(); 01568 map { $_->print($fh, '', $id_map); } @$accts; 01569 undef $accts; 01570 print $fh "\n"; 01571 01572 print $fh "#### Categories ####\n"; 01573 my $cats = $self->get_cats(); 01574 map { $_->print($fh, '', $id_map); } @$cats; 01575 undef $cats; 01576 print $fh "\n"; 01577 } 01578 01579 sub print_txns { 01580 my ($self, $fh, $id_map) = @_; 01581 print $fh "#### Transactions ####\n\n"; 01582 my $txns = $self->get_txns(); 01583 my $txn; 01584 foreach $txn (@$txns) { 01585 $txn->print($fh, '',$id_map); 01586 print $fh "\n"; 01587 } 01588 } 01589 01590 sub print { 01591 my($self, $fh) = @_; 01592 01593 my %id_map; 01594 my $i = 0; 01595 my $accts = $self->get_accts(); 01596 map { 01597 $id_map{$_} = "a$i"; 01598 $i++; 01599 } @$accts; 01600 $i = 0; 01601 my $cats = $self->get_cats(); 01602 map { 01603 $id_map{$_} = "c$i"; 01604 $i++; 01605 } @$cats; 01606 01607 print $fh "# CBB data file\n"; 01608 print $fh "Version: 1.0\n"; 01609 print $fh 'Default-sink: ' . $id_map{$self->get_default_sink()} . "\n"; 01610 print $fh "\n"; 01611 01612 $self->print_sinks($fh, \%id_map); 01613 $self->print_txns($fh, \%id_map); 01614 } 01615 01616 package CBBlib; 01617 01618 sub key_colon_value_to_hash { 01619 my($text) = @_; 01620 # Assumes comment lines have already been stripped. 01621 01622 my @lines = split("\n", $text); 01623 my %data; 01624 01625 map { 01626 if($_ =~ m/\s*([^:]+):\s*(.*)$/o) { 01627 $data{$1} = $2; 01628 } else { 01629 die 'Bad line in database file, first "key: value" section.'; 01630 } 01631 } @lines; 01632 return \%data; 01633 } 01634 01635 sub load_file { 01636 # Args (filename:<string>) 01637 01638 elapsed_reset("Starting load"); 01639 my $name = shift; 01640 my $categories; 01641 my $accounts; 01642 my @transactions = (); 01643 my $fh = new IO::File; 01644 my $file; 01645 01646 $fh->open($name) or die "Can't open input data file $file."; 01647 $fh->input_record_separator(''); 01648 01649 # Get the initial key/value pairs. 01650 my $text = <$fh>; 01651 $text =~ s/#.*//mgo; # Kill comment lines. 01652 $text =~ s/^\n//mgo; # Kill blank lines. 01653 my $file_data = key_colon_value_to_hash($text); 01654 01655 die "Couldn't determine data file version" 01656 unless $$file_data{'Version'}; 01657 die "Couldn't find default sink in $text." 01658 unless $$file_data{'Default-sink'}; 01659 01660 my %sink_map = (); 01661 01662 # Get accounts 01663 $text = <$fh>; 01664 $text =~ s/#.*\n//mgo; # Kill comment lines. 01665 my @sinks = CBBlib::Db::extract_accounts_($text, \%sink_map); 01666 01667 # Get categories 01668 $text = <$fh>; 01669 $text =~ s/#.*\n//mgo; # Kill comment lines. 01670 push @sinks, CBBlib::Db::extract_categories_($text, \%sink_map); 01671 01672 my $default_sink = $sink_map{$$file_data{'Default-sink'}}; 01673 die "Default sink " . $$file_data{'Default-sink'} . 01674 " not in database file." unless $default_sink; 01675 01676 my $self = new CBBlib::Db($default_sink); 01677 01678 $self->add_sinks(\@sinks); 01679 01680 01681 my @new_txns = (); 01682 01683 # Read the "Transactions" comment line. 01684 scalar(<$fh>); 01685 01686 my $count = 1; 01687 while(<$fh>) { 01688 print "\rLoading record $count"; 01689 $count++; 01690 01691 my @lines = split('\n', $_); 01692 my $transaction_info = shift @lines; 01693 my($date, $source, $checknum, $description, $status) = 01694 split("\t", $transaction_info); 01695 $source = $sink_map{$source}; 01696 01697 if(!defined($status) || $status eq '') { $status = ' '; } 01698 01699 my $transaction = 01700 new CBBlib::Txn($date, $source, $checknum, $description, $status); 01701 01702 # @lines is now just the split lines 01703 my $split_line; 01704 foreach $split_line (@lines) { 01705 $split_line =~ s/^\s*//o; 01706 my($destination, $note, $debit, $credit, $status) = 01707 split("\t", $split_line); 01708 if($destination) { 01709 my $old_dest = $destination; 01710 $destination = $sink_map{$destination}; 01711 if(!$destination) { 01712 die "No destination for key [$old_dest]\n"; 01713 } 01714 } else { 01715 $destination = $self->get_default_sink(); 01716 } 01717 01718 $transaction->add_split(new CBBlib::Split($destination, $note, 01719 $debit, $credit, $status)); 01720 } 01721 push @new_txns, $transaction; 01722 } 01723 print "\n"; 01724 elapsed(); 01725 elapsed_reset(" Now adding transactions"); 01726 $self->add_txns(\@new_txns); 01727 elapsed(); 01728 elapsed_reset(" Calculating totals"); 01729 $self->calc_account_totals_only_(); 01730 elapsed(); 01731 $fh->close() or die "Can't open close file $name."; 01732 print STDERR "finished load\n"; 01733 01734 return $self; 01735 } 01736 01737 1; 01738 __END__ 01739 ## @endcond Perl
1.7.4