1 package OpenILS::Application::Circ::CircCommon;
2 use strict; use warnings;
4 use DateTime::Format::ISO8601;
5 use OpenILS::Application::AppUtils;
6 use OpenSRF::Utils qw/:datetime/;
8 use OpenSRF::Utils::Logger qw(:logger);
9 use OpenILS::Utils::CStoreEditor q/:funcs/;
10 use OpenILS::Const qw/:const/;
12 my $U = "OpenILS::Application::AppUtils";
14 # -----------------------------------------------------------------
15 # Do not publish methods here. This code is shared across apps.
16 # -----------------------------------------------------------------
19 # -----------------------------------------------------------------
20 # Voids overdue fines on the given circ. if a backdate is
21 # provided, then we only void back to the backdate, unless the
22 # backdate is to within the grace period, in which case we void all
24 # -----------------------------------------------------------------
26 my($class, $e, $circ, $backdate, $note) = @_;
34 # ------------------------------------------------------------------
35 # Fines for overdue materials are assessed up to, but not including,
36 # one fine interval after the fines are applicable. Here, we add
37 # one fine interval to the backdate to ensure that we are not
38 # voiding fines that were applicable before the backdate.
39 # ------------------------------------------------------------------
41 # if there is a raw time component (e.g. from postgres),
42 # turn it into an interval that interval_to_seconds can parse
43 my $duration = $circ->fine_interval;
44 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
45 my $interval = OpenSRF::Utils->interval_to_seconds($duration);
47 my $date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($backdate));
48 my $due_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->due_date))->epoch;
49 my $grace_period = extend_grace_period( $class, $circ->circ_lib, $circ->due_date, OpenSRF::Utils->interval_to_seconds($circ->grace_period), $e);
50 if($date->epoch <= $due_date + $grace_period) {
51 $logger->info("backdate $backdate is within grace period, voiding all");
53 $backdate = $U->epoch2ISO8601($date->epoch + $interval);
54 $logger->info("applying backdate $backdate in overdue voiding");
55 $$bill_search{billing_ts} = {'>=' => $backdate};
59 my $bills = $e->search_money_billing($bill_search);
61 for my $bill (@$bills) {
62 next if $U->is_true($bill->voided);
63 $logger->info("voiding overdue bill ".$bill->id);
65 $bill->void_time('now');
66 $bill->voider($e->requestor->id);
67 my $n = ($bill->note) ? sprintf("%s\n", $bill->note) : "";
68 $bill->note(sprintf("$n%s", ($note) ? $note : "System: VOIDED FOR BACKDATE"));
69 $e->update_money_billing($bill) or return $e->die_event;
75 # ------------------------------------------------------------------
76 # remove charge from patron's account if lost item is returned
77 # ------------------------------------------------------------------
79 my ($class, $e, $circ, $btype) = @_;
81 my $bills = $e->search_money_billing(
89 $logger->debug("voiding lost item charge of ".scalar(@$bills));
90 for my $bill (@$bills) {
91 if( !$U->is_true($bill->voided) ) {
92 $logger->info("lost item returned - voiding bill ".$bill->id);
94 $bill->void_time('now');
95 $bill->voider($e->requestor->id);
96 my $note = ($bill->note) ? $bill->note . "\n" : '';
97 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
100 unless $e->update_money_billing($bill);
107 my($class, $e, $xactid) = @_;
109 # -----------------------------------------------------------------
110 # make sure the transaction is not closed
111 my $xact = $e->retrieve_money_billable_transaction($xactid)
112 or return $e->die_event;
114 if( $xact->xact_finish ) {
115 my ($mbts) = $U->fetch_mbts($xactid, $e);
116 if( $mbts->balance_owed != 0 ) {
117 $logger->info("* re-opening xact $xactid, orig xact_finish is ".$xact->xact_finish);
118 $xact->clear_xact_finish;
119 $e->update_money_billable_transaction($xact)
120 or return $e->die_event;
129 my($class, $e, $amount, $btype, $type, $xactid, $note) = @_;
131 $logger->info("The system is charging $amount [$type] on xact $xactid");
132 $note ||= 'SYSTEM GENERATED';
134 # -----------------------------------------------------------------
135 # now create the billing
136 my $bill = Fieldmapper::money::billing->new;
137 $bill->xact($xactid);
138 $bill->amount($amount);
139 $bill->billing_type($type);
140 $bill->btype($btype);
142 $e->create_money_billing($bill) or return $e->die_event;
147 sub extend_grace_period {
148 my($class, $circ_lib, $due_date, $grace_period, $e, $h) = @_;
149 if ($grace_period >= 86400) { # Only extend grace periods greater than or equal to a full day
150 my $parser = DateTime::Format::ISO8601->new;
151 my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $due_date ) );
152 my $due = $due_dt->epoch;
154 my $grace_extend = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend');
155 $e = new_editor() if (!$e);
156 $h = $e->retrieve_actor_org_unit_hours_of_operation($circ_lib) if (!$h);
157 if ($grace_extend and $h) {
158 my $new_grace_period = $grace_period;
160 $logger->info( "Circ lib has an hours-of-operation entry and grace period extension is enabled." );
165 my $dow_open = "dow_${i}_open";
166 my $dow_close = "dow_${i}_close";
167 if($h->$dow_open() eq '00:00:00' and $h->$dow_close() eq '00:00:00') {
176 $logger->info("Circ lib is closed all week according to hours-of-operation entry. Skipping grace period extension checks.");
178 # Extra nice grace periods
179 # AKA, merge closed dates trailing the grace period into the grace period
180 my $grace_extend_into_closed = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend.into_closed');
181 $due += 86400 if $grace_extend_into_closed;
183 my $grace_extend_all = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend.all');
185 if ( $grace_extend_all ) {
186 # Start checking the day after the item was due
187 # This is "The grace period only counts open days"
188 # NOTE: Adding 86400 seconds is not the same as adding one day. This uses seconds intentionally.
189 $due_dt = $due_dt->add( seconds => 86400 );
191 # Jump to the end of the grace period
192 # This is "If the grace period ends on a closed day extend it"
193 # NOTE: This adds grace period as a number of seconds intentionally
194 $due_dt = $due_dt->add( seconds => $grace_period );
197 my $count = 0; # Infinite loop protection
199 $closed = 0; # Starting assumption for day: We are not closed
200 $count++; # We limit the number of loops below.
202 # get the day of the week for the day we are looking at
203 my $dow = $due_dt->day_of_week_0;
205 # Check hours of operation first.
206 if ($h_closed{$dow}) {
208 $new_grace_period += 86400;
209 $due_dt->add( seconds => 86400 );
211 # Check for closed dates for this period
212 my $timestamptz = $due_dt->strftime('%FT%T%z');
213 my $cl = $e->search_actor_org_unit_closed_date(
214 { close_start => { '<=' => $timestamptz },
215 close_end => { '>=' => $timestamptz },
216 org_unit => $circ_lib }
221 my $cl_dt = $parser->parse_datetime( cleanse_ISO8601( $_->close_end ) );
222 while ($due_dt <= $cl_dt) {
223 $due_dt->add( seconds => 86400 );
224 $new_grace_period += 86400;
228 $due_dt->add( seconds => 86400 );
231 } while ( $count <= 366 and ( $closed or $due_dt->epoch <= $due + $new_grace_period ) );
232 if ($new_grace_period > $grace_period) {
233 $grace_period = $new_grace_period;
234 $logger->info( "Grace period for circ extended to $grace_period [" . seconds_to_interval( $grace_period ) . "]" );
239 return $grace_period;
242 # check if a circulation transaction can be closed
243 # takes a CStoreEditor and a circ transaction.
244 # Returns 1 if the circ should be closed, 0 if not.
246 my ($class, $e, $circ) = @_;
249 my $reason = $circ->stop_fines;
251 # We definitely want to close if this circulation was
252 # checked in or renewed.
253 if ($circ->checkin_time) {
255 } elsif ($reason eq OILS_STOP_FINES_LOST) {
256 # Check the copy circ_lib to see if they close
257 # transactions when lost are paid.
258 my $copy = $e->retrieve_asset_copy($circ->target_copy);
260 $can_close = !$U->is_true(
261 $U->ou_ancestor_setting_value(
263 'circ.lost.xact_open_on_zero',
269 } elsif ($reason eq OILS_STOP_FINES_LONGOVERDUE) {
270 # Check the copy circ_lib to see if they close
271 # transactions when long-overdue are paid.
272 my $copy = $e->retrieve_asset_copy($circ->target_copy);
274 $can_close = !$U->is_true(
275 $U->ou_ancestor_setting_value(
277 'circ.longoverdue.xact_open_on_zero',