From f901121f7a163c75ab87256bfb7e4551d3f4362c Mon Sep 17 00:00:00 2001 From: Dan Wells Date: Mon, 24 Nov 2014 17:10:31 -0500 Subject: [PATCH] LP#1198465 Initial copy of generate_fines to CircCommon.pm Copy over generate_fines, including the necessary 'ceil' and $parser bits, and the seconds_to_interval_hash helper function. Signed-off-by: Dan Wells --- .../lib/OpenILS/Application/Circ/CircCommon.pm | 326 +++++++++++++++++++++ 1 file changed, 326 insertions(+) diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm index f2d5aa5dbf..e7acd5fce6 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm @@ -8,8 +8,10 @@ use OpenILS::Event; use OpenSRF::Utils::Logger qw(:logger); use OpenILS::Utils::CStoreEditor q/:funcs/; use OpenILS::Const qw/:const/; +use POSIX qw(ceil); my $U = "OpenILS::Application::AppUtils"; +my $parser = DateTime::Format::ISO8601->new; # ----------------------------------------------------------------- # Do not publish methods here. This code is shared across apps. @@ -284,4 +286,328 @@ sub can_close_circ { return $can_close; } +sub seconds_to_interval_hash { + my $interval = shift; + my $limit = shift || 's'; + $limit =~ s/^(.)/$1/o; + + my %output; + + my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s); + my ($year, $month, $week, $day, $hour, $minute, $second) = + ('years','months','weeks','days', 'hours', 'minutes', 'seconds'); + + if ($y = int($interval / (60 * 60 * 24 * 365))) { + $output{$year} = $y; + $ym = $interval % (60 * 60 * 24 * 365); + } else { + $ym = $interval; + } + return %output if ($limit eq 'y'); + + if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) { + $output{$month} = $M; + $Mm = $ym % ((60 * 60 * 24 * 365)/12); + } else { + $Mm = $ym; + } + return %output if ($limit eq 'M'); + + if ($w = int($Mm / 604800)) { + $output{$week} = $w; + $wm = $Mm % 604800; + } else { + $wm = $Mm; + } + return %output if ($limit eq 'w'); + + if ($d = int($wm / 86400)) { + $output{$day} = $d; + $dm = $wm % 86400; + } else { + $dm = $wm; + } + return %output if ($limit eq 'd'); + + if ($h = int($dm / 3600)) { + $output{$hour} = $h; + $hm = $dm % 3600; + } else { + $hm = $dm; + } + return %output if ($limit eq 'h'); + + if ($m = int($hm / 60)) { + $output{$minute} = $m; + $mm = $hm % 60; + } else { + $mm = $hm; + } + return %output if ($limit eq 'm'); + + if ($s = int($mm)) { + $output{$second} = $s; + } else { + $output{$second} = 0 unless (keys %output); + } + return %output; +} + +sub generate_fines { + my $self = shift; + my $client = shift; + my $circ = shift; + my $stop_fines_reasons = shift; + + local $OpenILS::Application::Storage::WRITE = 1; + + my @circs; + if ($circ) { + push @circs, + action::circulation->search_where( { id => $circ, stop_fines => $stop_fines_reasons } ), + booking::reservation->search_where( { id => $circ, return_time => undef, cancel_time => undef } ); + } else { + push @circs, overdue_circs(); + } + + my %hoo = map { ( $_->id => $_ ) } actor::org_unit::hours_of_operation->retrieve_all; + + my $penalty = OpenSRF::AppSession->create('open-ils.penalty'); + my $handling_resvs = 0; + for my $c (@$circs) { + + my $ctype = ref($c); + + if (!$ctype) { # fetched via idlist + if ($handling_resvs) { + $c = booking::reservation->retrieve($c); + } elsif (not defined $c) { + # an undef value is the indicator that we are moving + # from processing circulations to reservations. + $handling_resvs = 1; + next; + } else { + $c = action::circulation->retrieve($c); + } + $ctype = ref($c); + } + + $ctype =~ s/^.+::(\w+)$/$1/; + + my $due_date_method = 'due_date'; + my $target_copy_method = 'target_copy'; + my $circ_lib_method = 'circ_lib'; + my $recurring_fine_method = 'recurring_fine'; + my $is_reservation = 0; + if ($ctype eq 'reservation') { + $is_reservation = 1; + $due_date_method = 'end_time'; + $target_copy_method = 'current_resource'; + $circ_lib_method = 'pickup_lib'; + $recurring_fine_method = 'fine_amount'; + next unless ($c->fine_interval); + } + #TODO: reservation grace periods + my $grace_period = ($is_reservation ? 0 : interval_to_seconds($c->grace_period)); + + eval { + if ($self->method_lookup('open-ils.storage.transaction.current')->run) { + $log->debug("Cleaning up after previous transaction\n"); + $self->method_lookup('open-ils.storage.transaction.rollback')->run; + } + $self->method_lookup('open-ils.storage.transaction.begin')->run( $client ); + $log->info( + sprintf("Processing %s %d...", + ($is_reservation ? "reservation" : "circ"), $c->id + ) + ); + + + my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->$due_date_method ) ); + + my $due = $due_dt->epoch; + my $now = time; + + my $fine_interval = $c->fine_interval; + $fine_interval =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o; + $fine_interval = interval_to_seconds( $fine_interval ); + + if ( $fine_interval == 0 || int($c->$recurring_fine_method * 100) == 0 || int($c->max_fine * 100) == 0 ) { + $client->respond( "Fine Generator skipping circ due to 0 fine interval, 0 fine rate, or 0 max fine.\n" ); + $log->info( "Fine Generator skipping circ " . $c->id . " due to 0 fine interval, 0 fine rate, or 0 max fine." ); + return; + } + + if ( $is_reservation and $fine_interval >= interval_to_seconds('1d') ) { + my $tz_offset_s = 0; + if ($due_dt->strftime('%z') =~ /(-|\+)(\d{2}):?(\d{2})/) { + $tz_offset_s = $1 . interval_to_seconds( "${2}h ${3}m"); + } + + $due -= ($due % $fine_interval) + $tz_offset_s; + $now -= ($now % $fine_interval) + $tz_offset_s; + } + + $client->respond( + "ARG! Overdue $ctype ".$c->id. + " for item ".$c->$target_copy_method. + " (user ".$c->usr.").\n". + "\tItem was due on or before: ".localtime($due)."\n"); + + my @fines = money::billing->search_where( + { xact => $c->id, + btype => 1, + billing_ts => { '>' => $c->$due_date_method } }, + { order_by => 'billing_ts DESC'} + ); + + my $f_idx = 0; + my $fine = $fines[$f_idx] if (@fines); + my $current_fine_total = 0; + $current_fine_total += int($_->amount * 100) for (grep { $_ and !$_->voided } @fines); + + my $last_fine; + if ($fine) { + $client->respond( "Last billing time: ".$fine->billing_ts." (clensed format: ".cleanse_ISO8601( $fine->billing_ts ).")"); + $last_fine = $parser->parse_datetime( cleanse_ISO8601( $fine->billing_ts ) )->epoch; + } else { + $log->info( "Potential first billing for circ ".$c->id ); + $last_fine = $due; + + $grace_period = OpenILS::Application::Circ::CircCommon->extend_grace_period($c->$circ_lib_method->to_fieldmapper->id,$c->$due_date_method,$grace_period,undef,$hoo{$c->$circ_lib_method}); + } + + return if ($last_fine > $now); + # Generate fines for each past interval, including the one we are inside + my $pending_fine_count = ceil( ($now - $last_fine) / $fine_interval ); + + if ( $last_fine == $due # we have no fines yet + && $grace_period # and we have a grace period + && $now < $due + $grace_period # and some date math says were are within the grace period + ) { + $client->respond( "Still inside grace period of: ". seconds_to_interval( $grace_period )."\n" ); + $log->info( "Circ ".$c->id." is still inside grace period of: $grace_period [". seconds_to_interval( $grace_period ).']' ); + return; + } + + $client->respond( "\t$pending_fine_count pending fine(s)\n" ); + return unless ($pending_fine_count); + + my $recurring_fine = int($c->$recurring_fine_method * 100); + my $max_fine = int($c->max_fine * 100); + + my $skip_closed_check = $U->ou_ancestor_setting_value( + $c->$circ_lib_method->to_fieldmapper->id, 'circ.fines.charge_when_closed'); + $skip_closed_check = $U->is_true($skip_closed_check); + + my $truncate_to_max_fine = $U->ou_ancestor_setting_value( + $c->$circ_lib_method->to_fieldmapper->id, 'circ.fines.truncate_to_max_fine'); + $truncate_to_max_fine = $U->is_true($truncate_to_max_fine); + + my ($latest_billing_ts, $latest_amount) = ('',0); + for (my $bill = 1; $bill <= $pending_fine_count; $bill++) { + + if ($current_fine_total >= $max_fine) { + $c->update({stop_fines => 'MAXFINES', stop_fines_time => 'now'}) if ($ctype eq 'circulation'); + $client->respond( + "\tMaximum fine level of ".$c->max_fine. + " reached for this $ctype.\n". + "\tNo more fines will be generated.\n" ); + last; + } + + # XXX Use org time zone (or default to 'local') once we have the ou setting built for that + my $billing_ts = DateTime->from_epoch( epoch => $last_fine, time_zone => 'local' ); + my $current_bill_count = $bill; + while ( $current_bill_count ) { + $billing_ts->add( seconds_to_interval_hash( $fine_interval ) ); + $current_bill_count--; + } + + my $timestamptz = $billing_ts->strftime('%FT%T%z'); + if (!$skip_closed_check) { + my $dow = $billing_ts->day_of_week_0(); + my $dow_open = "dow_${dow}_open"; + my $dow_close = "dow_${dow}_close"; + + if (my $h = $hoo{$c->$circ_lib_method}) { + next if ( $h->$dow_open eq '00:00:00' and $h->$dow_close eq '00:00:00'); + } + + my @cl = actor::org_unit::closed_date->search_where( + { close_start => { '<=' => $timestamptz }, + close_end => { '>=' => $timestamptz }, + org_unit => $c->$circ_lib_method } + ); + next if (@cl); + } + + # The billing amount for this billing normally ought to be the recurring fine amount. + # However, if the recurring fine amount would cause total fines to exceed the max fine amount, + # we may wish to reduce the amount for this billing (if circ.fines.truncate_to_max_fine is true). + my $this_billing_amount = $recurring_fine; + if ( $truncate_to_max_fine && ($current_fine_total + $this_billing_amount) > $max_fine ) { + $this_billing_amount = ($max_fine - $current_fine_total); + } + $current_fine_total += $this_billing_amount; + $latest_amount += $this_billing_amount; + $latest_billing_ts = $timestamptz; + + money::billing->create( + { xact => ''.$c->id, + note => "System Generated Overdue Fine", + billing_type => "Overdue materials", + btype => 1, + amount => sprintf('%0.2f', $this_billing_amount/100), + billing_ts => $timestamptz, + } + ); + + } + + $client->respond( "\t\tAdding fines totaling $latest_amount for overdue up to $latest_billing_ts\n" ) + if ($latest_billing_ts and $latest_amount); + + $self->method_lookup('open-ils.storage.transaction.commit')->run; + + if(1) { + + # Caluclate penalties inline + OpenILS::Utils::Penalty->calculate_penalties( + undef, $c->usr->to_fieldmapper->id.'', $c->$circ_lib_method->to_fieldmapper->id.''); + + } else { + + # Calculate penalties with an aysnc call to the penalty server. This approach + # may lead to duplicate penalties since multiple penalty processes for a + # given user may be running at the same time. Leave this here for reference + # in case we later find that asyc calls are needed in some environments. + $penalty->request( + 'open-ils.penalty.patron_penalty.calculate', + { patronid => ''.$c->usr, + context_org => ''.$c->$circ_lib_method, + update => 1, + background => 1, + } + )->gather(1); + } + + }; + + if ($@) { + my $e = $@; + $client->respond( "Error processing overdue $ctype [".$c->id."]:\n\n$e\n" ); + $log->error("Error processing overdue $ctype [".$c->id."]:\n$e\n"); + $self->method_lookup('open-ils.storage.transaction.rollback')->run; + last if ($e =~ /IS NOT CONNECTED TO THE NETWORK/o); + } + } +} +__PACKAGE__->register_method( + api_name => 'open-ils.storage.action.circulation.overdue.generate_fines', + api_level => 1, + stream => 1, + method => 'generate_fines', +); + 1; -- 2.11.0