]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
LP#1484989 Don't close xacts with checkin-generated fines
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
10 use DateTime;
11 my $U = "OpenILS::Application::AppUtils";
12
13 my %scripts;
14 my $booking_status;
15 my $opac_renewal_use_circ_lib;
16 my $desk_renewal_use_circ_lib;
17
18 sub determine_booking_status {
19     unless (defined $booking_status) {
20         my $ses = create OpenSRF::AppSession("router");
21         $booking_status = grep {$_ eq "open-ils.booking"} @{
22             $ses->request("opensrf.router.info.class.list")->gather(1)
23         };
24         $ses->disconnect;
25         $logger->info("booking status: " . ($booking_status ? "on" : "off"));
26     }
27
28     return $booking_status;
29 }
30
31
32 my $MK_ENV_FLESH = { 
33     flesh => 2, 
34     flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
35 };
36
37 sub initialize {}
38
39 __PACKAGE__->register_method(
40     method  => "run_method",
41     api_name    => "open-ils.circ.checkout.permit",
42     notes       => q/
43         Determines if the given checkout can occur
44         @param authtoken The login session key
45         @param params A trailing hash of named params including 
46             barcode : The copy barcode, 
47             patron : The patron the checkout is occurring for, 
48             renew : true or false - whether or not this is a renewal
49         @return The event that occurred during the permit check.  
50     /);
51
52
53 __PACKAGE__->register_method (
54     method      => 'run_method',
55     api_name        => 'open-ils.circ.checkout.permit.override',
56     signature   => q/@see open-ils.circ.checkout.permit/,
57 );
58
59
60 __PACKAGE__->register_method(
61     method  => "run_method",
62     api_name    => "open-ils.circ.checkout",
63     notes => q/
64         Checks out an item
65         @param authtoken The login session key
66         @param params A named hash of params including:
67             copy            The copy object
68             barcode     If no copy is provided, the copy is retrieved via barcode
69             copyid      If no copy or barcode is provide, the copy id will be use
70             patron      The patron's id
71             noncat      True if this is a circulation for a non-cataloted item
72             noncat_type The non-cataloged type id
73             noncat_circ_lib The location for the noncat circ.  
74             precat      The item has yet to be cataloged
75             dummy_title The temporary title of the pre-cataloded item
76             dummy_author The temporary authr of the pre-cataloded item
77                 Default is the home org of the staff member
78         @return The SUCCESS event on success, any other event depending on the error
79     /);
80
81 __PACKAGE__->register_method(
82     method  => "run_method",
83     api_name    => "open-ils.circ.checkin",
84     argc        => 2,
85     signature   => q/
86         Generic super-method for handling all copies
87         @param authtoken The login session key
88         @param params Hash of named parameters including:
89             barcode - The copy barcode
90             force   - If true, copies in bad statuses will be checked in and give good statuses
91             noop    - don't capture holds or put items into transit
92             void_overdues - void all overdues for the circulation (aka amnesty)
93             ...
94     /
95 );
96
97 __PACKAGE__->register_method(
98     method    => "run_method",
99     api_name  => "open-ils.circ.checkin.override",
100     signature => q/@see open-ils.circ.checkin/
101 );
102
103 __PACKAGE__->register_method(
104     method    => "run_method",
105     api_name  => "open-ils.circ.renew.override",
106     signature => q/@see open-ils.circ.renew/,
107 );
108
109
110 __PACKAGE__->register_method(
111     method  => "run_method",
112     api_name    => "open-ils.circ.renew",
113     notes       => <<"    NOTES");
114     PARAMS( authtoken, circ => circ_id );
115     open-ils.circ.renew(login_session, circ_object);
116     Renews the provided circulation.  login_session is the requestor of the
117     renewal and if the logged in user is not the same as circ->usr, then
118     the logged in user must have RENEW_CIRC permissions.
119     NOTES
120
121 __PACKAGE__->register_method(
122     method   => "run_method",
123     api_name => "open-ils.circ.checkout.full"
124 );
125 __PACKAGE__->register_method(
126     method   => "run_method",
127     api_name => "open-ils.circ.checkout.full.override"
128 );
129 __PACKAGE__->register_method(
130     method   => "run_method",
131     api_name => "open-ils.circ.reservation.pickup"
132 );
133 __PACKAGE__->register_method(
134     method   => "run_method",
135     api_name => "open-ils.circ.reservation.return"
136 );
137 __PACKAGE__->register_method(
138     method   => "run_method",
139     api_name => "open-ils.circ.reservation.return.override"
140 );
141 __PACKAGE__->register_method(
142     method   => "run_method",
143     api_name => "open-ils.circ.checkout.inspect",
144     desc     => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
145 );
146
147
148 sub run_method {
149     my( $self, $conn, $auth, $args ) = @_;
150     translate_legacy_args($args);
151     $args->{override_args} = { all => 1 } unless defined $args->{override_args};
152     my $api = $self->api_name;
153
154     my $circulator = 
155         OpenILS::Application::Circ::Circulator->new($auth, %$args);
156
157     return circ_events($circulator) if $circulator->bail_out;
158
159     $circulator->use_booking(determine_booking_status());
160
161     # --------------------------------------------------------------------------
162     # First, check for a booking transit, as the barcode may not be a copy
163     # barcode, but a resource barcode, and nothing else in here will work
164     # --------------------------------------------------------------------------
165
166     if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
167         my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
168         if (@$resources) { # yes!
169
170             my $res_id_list = [ map { $_->id } @$resources ];
171             my $transit = $circulator->editor->search_action_reservation_transit_copy(
172                 [
173                     { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
174                     { order_by => { artc => 'source_send_time' }, limit => 1 }
175                 ]
176             )->[0]; # Any transit for this barcode?
177
178             if ($transit) { # yes! unwrap it.
179
180                 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
181                 my $res_type    = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
182
183                 my $success_event = new OpenILS::Event(
184                     "SUCCESS", "payload" => {"reservation" => $reservation}
185                 );
186                 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
187                     if (my $copy = $circulator->editor->search_asset_copy([
188                         { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
189                     ])->[0]) { # got a copy
190                         $copy->status( $transit->copy_status );
191                         $copy->editor($circulator->editor->requestor->id);
192                         $copy->edit_date('now');
193                         $circulator->editor->update_asset_copy($copy);
194                         $success_event->{"payload"}->{"record"} =
195                             $U->record_to_mvr($copy->call_number->record);
196                         $success_event->{"payload"}->{"volume"} = $copy->call_number;
197                         $copy->call_number($copy->call_number->id);
198                         $success_event->{"payload"}->{"copy"} = $copy;
199                     }
200                 }
201
202                 $transit->dest_recv_time('now');
203                 $circulator->editor->update_action_reservation_transit_copy( $transit );
204
205                 $circulator->editor->commit;
206                 # Formerly this branch just stopped here. Argh!
207                 $conn->respond_complete($success_event);
208                 return;
209             }
210         }
211     }
212
213     if ($circulator->use_booking) {
214         $circulator->is_res_checkin($circulator->is_checkin(1))
215             if $api =~ /reservation.return/ or (
216                 $api =~ /checkin/ and $circulator->seems_like_reservation()
217             );
218
219         $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
220     }
221
222     $circulator->is_renewal(1) if $api =~ /renew/;
223     $circulator->is_checkin(1) if $api =~ /checkin/;
224
225     $circulator->mk_env();
226     $circulator->noop(1) if $circulator->claims_never_checked_out;
227
228     return circ_events($circulator) if $circulator->bail_out;
229     
230     $circulator->override(1) if $api =~ /override/o;
231
232     if( $api =~ /checkout\.permit/ ) {
233         $circulator->do_permit();
234
235     } elsif( $api =~ /checkout.full/ ) {
236
237         # requesting a precat checkout implies that any required
238         # overrides have been performed.  Go ahead and re-override.
239         $circulator->skip_permit_key(1);
240         $circulator->override(1) if $circulator->request_precat;
241         $circulator->do_permit();
242         $circulator->is_checkout(1);
243         unless( $circulator->bail_out ) {
244             $circulator->events([]);
245             $circulator->do_checkout();
246         }
247
248     } elsif( $circulator->is_res_checkout ) {
249         $circulator->do_reservation_pickup();
250
251     } elsif( $api =~ /inspect/ ) {
252         my $data = $circulator->do_inspect();
253         $circulator->editor->rollback;
254         return $data;
255
256     } elsif( $api =~ /checkout/ ) {
257         $circulator->is_checkout(1);
258         $circulator->do_checkout();
259
260     } elsif( $circulator->is_res_checkin ) {
261         $circulator->do_reservation_return();
262         $circulator->do_checkin() if ($circulator->copy());
263     } elsif( $api =~ /checkin/ ) {
264         $circulator->do_checkin();
265
266     } elsif( $api =~ /renew/ ) {
267         $circulator->is_renewal(1);
268         $circulator->do_renew();
269     }
270
271     if( $circulator->bail_out ) {
272
273         my @ee;
274         # make sure no success event accidentally slip in
275         $circulator->events(
276             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
277
278         # Log the events
279         my @e = @{$circulator->events};
280         push( @ee, $_->{textcode} ) for @e;
281         $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
282
283         $circulator->editor->rollback;
284
285     } else {
286
287         $circulator->editor->commit;
288     }
289     
290     $conn->respond_complete(circ_events($circulator));
291
292     return undef if $circulator->bail_out;
293
294     $circulator->do_hold_notify($circulator->notify_hold)
295         if $circulator->notify_hold;
296     $circulator->retarget_holds if $circulator->retarget;
297     $circulator->append_reading_list;
298     $circulator->make_trigger_events;
299     
300     return undef;
301 }
302
303 sub circ_events {
304     my $circ = shift;
305     my @e = @{$circ->events};
306     # if we have multiple events, SUCCESS should not be one of them;
307     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
308     return (@e == 1) ? $e[0] : \@e;
309 }
310
311
312 sub translate_legacy_args {
313     my $args = shift;
314
315     if( $$args{barcode} ) {
316         $$args{copy_barcode} = $$args{barcode};
317         delete $$args{barcode};
318     }
319
320     if( $$args{copyid} ) {
321         $$args{copy_id} = $$args{copyid};
322         delete $$args{copyid};
323     }
324
325     if( $$args{patronid} ) {
326         $$args{patron_id} = $$args{patronid};
327         delete $$args{patronid};
328     }
329
330     if( $$args{patron} and !ref($$args{patron}) ) {
331         $$args{patron_id} = $$args{patron};
332         delete $$args{patron};
333     }
334
335
336     if( $$args{noncat} ) {
337         $$args{is_noncat} = $$args{noncat};
338         delete $$args{noncat};
339     }
340
341     if( $$args{precat} ) {
342         $$args{is_precat} = $$args{request_precat} = $$args{precat};
343         delete $$args{precat};
344     }
345 }
346
347
348
349 # --------------------------------------------------------------------------
350 # This package actually manages all of the circulation logic
351 # --------------------------------------------------------------------------
352 package OpenILS::Application::Circ::Circulator;
353 use strict; use warnings;
354 use vars q/$AUTOLOAD/;
355 use DateTime;
356 use OpenILS::Utils::Fieldmapper;
357 use OpenSRF::Utils::Cache;
358 use Digest::MD5 qw(md5_hex);
359 use DateTime::Format::ISO8601;
360 use OpenILS::Utils::PermitHold;
361 use OpenSRF::Utils qw/:datetime/;
362 use OpenSRF::Utils::SettingsClient;
363 use OpenILS::Application::Circ::Holds;
364 use OpenILS::Application::Circ::Transit;
365 use OpenSRF::Utils::Logger qw(:logger);
366 use OpenILS::Utils::CStoreEditor qw/:funcs/;
367 use OpenILS::Const qw/:const/;
368 use OpenILS::Utils::Penalty;
369 use OpenILS::Application::Circ::CircCommon;
370 use Time::Local;
371
372 my $CC = "OpenILS::Application::Circ::CircCommon";
373 my $holdcode    = "OpenILS::Application::Circ::Holds";
374 my $transcode   = "OpenILS::Application::Circ::Transit";
375 my %user_groups;
376
377 sub DESTROY { }
378
379
380 # --------------------------------------------------------------------------
381 # Add a pile of automagic getter/setter methods
382 # --------------------------------------------------------------------------
383 my @AUTOLOAD_FIELDS = qw/
384     notify_hold
385     remote_hold
386     backdate
387     reservation
388     copy
389     copy_id
390     copy_barcode
391     patron
392     patron_id
393     patron_barcode
394     volume
395     title
396     is_renewal
397     is_checkout
398     is_res_checkout
399     is_precat
400     is_noncat
401     request_precat
402     is_checkin
403     is_res_checkin
404     noncat_type
405     editor
406     events
407     cache_handle
408     override
409     circ_permit_patron
410     circ_permit_copy
411     circ_duration
412     circ_recurring_fines
413     circ_max_fines
414     circ_permit_renew
415     circ
416     transit
417     hold
418     permit_key
419     noncat_circ_lib
420     noncat_count
421     checkout_time
422     dummy_title
423     dummy_author
424     dummy_isbn
425     circ_modifier
426     circ_lib
427     barcode
428     duration_level
429     recurring_fines_level
430     duration_rule
431     recurring_fines_rule
432     max_fine_rule
433     renewal_remaining
434     hard_due_date
435     due_date
436     fulfilled_holds
437     transit
438     checkin_changed
439     force
440     permit_override
441     pending_checkouts
442     cancelled_hold_transit
443     opac_renewal
444     phone_renewal
445     desk_renewal
446     sip_renewal
447     retarget
448     matrix_test_result
449     circ_matrix_matchpoint
450     circ_test_success
451     is_deposit
452     is_rental
453     deposit_billing
454     rental_billing
455     capture
456     noop
457     void_overdues
458     parent_circ
459     return_patron
460     claims_never_checked_out
461     skip_permit_key
462     skip_deposit_fee
463     skip_rental_fee
464     use_booking
465     clear_expired
466     retarget_mode
467     hold_as_transit
468     fake_hold_dest
469     limit_groups
470     override_args
471     checkout_is_for_hold
472     manual_float
473     dont_change_lost_zero
474     lost_bill_options
475     needs_lost_bill_handling
476 /;
477
478
479 sub AUTOLOAD {
480     my $self = shift;
481     my $type = ref($self) or die "$self is not an object";
482     my $data = shift;
483     my $name = $AUTOLOAD;
484     $name =~ s/.*://o;   
485
486     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
487         $logger->error("circulator: $type: invalid autoload field: $name");
488         die "$type: invalid autoload field: $name\n" 
489     }
490
491     {
492         no strict 'refs';
493         *{"${type}::${name}"} = sub {
494             my $s = shift;
495             my $v = shift;
496             $s->{$name} = $v if defined $v;
497             return $s->{$name};
498         }
499     }
500     return $self->$name($data);
501 }
502
503
504 sub new {
505     my( $class, $auth, %args ) = @_;
506     $class = ref($class) || $class;
507     my $self = bless( {}, $class );
508
509     $self->events([]);
510     $self->editor(new_editor(xact => 1, authtoken => $auth));
511
512     unless( $self->editor->checkauth ) {
513         $self->bail_on_events($self->editor->event);
514         return $self;
515     }
516
517     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
518
519     $self->$_($args{$_}) for keys %args;
520
521     $self->circ_lib(
522         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
523
524     # if this is a renewal, default to desk_renewal
525     $self->desk_renewal(1) unless 
526         $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
527
528     $self->capture('') unless $self->capture;
529
530     unless(%user_groups) {
531         my $gps = $self->editor->retrieve_all_permission_grp_tree;
532         %user_groups = map { $_->id => $_ } @$gps;
533     }
534
535     return $self;
536 }
537
538
539 # --------------------------------------------------------------------------
540 # True if we should discontinue processing
541 # --------------------------------------------------------------------------
542 sub bail_out {
543     my( $self, $bool ) = @_;
544     if( defined $bool ) {
545         $logger->info("circulator: BAILING OUT") if $bool;
546         $self->{bail_out} = $bool;
547     }
548     return $self->{bail_out};
549 }
550
551
552 sub push_events {
553     my( $self, @evts ) = @_;
554     for my $e (@evts) {
555         next unless $e;
556         $e->{payload} = $self->copy if 
557               ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
558
559         $logger->info("circulator: pushing event ".$e->{textcode});
560         push( @{$self->events}, $e ) unless
561             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
562     }
563 }
564
565 sub mk_permit_key {
566     my $self = shift;
567     return '' if $self->skip_permit_key;
568     my $key = md5_hex( time() . rand() . "$$" );
569     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
570     return $self->permit_key($key);
571 }
572
573 sub check_permit_key {
574     my $self = shift;
575     return 1 if $self->skip_permit_key;
576     my $key = $self->permit_key;
577     return 0 unless $key;
578     my $k = "oils_permit_key_$key";
579     my $one = $self->cache_handle->get_cache($k);
580     $self->cache_handle->delete_cache($k);
581     return ($one) ? 1 : 0;
582 }
583
584 sub seems_like_reservation {
585     my $self = shift;
586
587     # Some words about the following method:
588     # 1) It requires the VIEW_USER permission, but that's not an
589     # issue, right, since all staff should have that?
590     # 2) It returns only one reservation at a time, even if an item can be
591     # and is currently overbooked.  Hmmm....
592     my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
593     my $result = $booking_ses->request(
594         "open-ils.booking.reservations.by_returnable_resource_barcode",
595         $self->editor->authtoken,
596         $self->copy_barcode
597     )->gather(1);
598     $booking_ses->disconnect;
599
600     return $self->bail_on_events($result) if defined $U->event_code($result);
601
602     if (@$result > 0) {
603         $self->reservation(shift @$result);
604         return 1;
605     } else {
606         return 0;
607     }
608
609 }
610
611 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
612 sub save_trimmed_copy {
613     my ($self, $copy) = @_;
614
615     $self->copy($copy);
616     $self->volume($copy->call_number);
617     $self->title($self->volume->record);
618     $self->copy->call_number($self->volume->id);
619     $self->volume->record($self->title->id);
620     $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
621     if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
622         $self->is_deposit(1) if $U->is_true($self->copy->deposit);
623         $self->is_rental(1) unless $U->is_true($self->copy->deposit);
624     }
625 }
626
627 sub mk_env {
628     my $self = shift;
629     my $e = $self->editor;
630
631     # --------------------------------------------------------------------------
632     # Grab the fleshed copy
633     # --------------------------------------------------------------------------
634     unless($self->is_noncat) {
635         my $copy;
636         if($self->copy_id) {
637             $copy = $e->retrieve_asset_copy(
638                 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
639     
640         } elsif( $self->copy_barcode ) {
641     
642             $copy = $e->search_asset_copy(
643                 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
644         } elsif( $self->reservation ) {
645             my $res = $e->json_query(
646                 {
647                     "select" => {"acp" => ["id"]},
648                     "from" => {
649                         "acp" => {
650                             "brsrc" => {
651                                 "fkey" => "barcode",
652                                 "field" => "barcode",
653                                 "join" => {
654                                     "bresv" => {
655                                         "fkey" => "id",
656                                         "field" => "current_resource"
657                                     }
658                                 }
659                             }
660                         }
661                     },
662                     "where" => {
663                         "+bresv" => {
664                             "id" => (ref $self->reservation) ?
665                                 $self->reservation->id : $self->reservation
666                         }
667                     }
668                 }
669             );
670             if (ref $res eq "ARRAY" and scalar @$res) {
671                 $logger->info("circulator: mapped reservation " .
672                     $self->reservation . " to copy " . $res->[0]->{"id"});
673                 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
674             }
675         }
676     
677         if($copy) {
678             $self->save_trimmed_copy($copy);
679         } else {
680             # We can't renew if there is no copy
681             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
682                 if $self->is_renewal;
683             $self->is_precat(1);
684         }
685     }
686
687     # --------------------------------------------------------------------------
688     # Grab the patron
689     # --------------------------------------------------------------------------
690     my $patron;
691     my $flesh = {
692         flesh => 1,
693         flesh_fields => {au => [ qw/ card / ]}
694     };
695
696     if( $self->patron_id ) {
697         $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
698             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
699
700     } elsif( $self->patron_barcode ) {
701
702         # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
703         my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0] 
704             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
705
706         $patron = $e->retrieve_actor_user($card->usr)
707             or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
708
709         # Use the card we looked up, not the patron's primary, for card active checks
710         $patron->card($card);
711
712     } else {
713         if( my $copy = $self->copy ) {
714
715             $flesh->{flesh} = 2;
716             $flesh->{flesh_fields}->{circ} = ['usr'];
717
718             my $circ = $e->search_action_circulation([
719                 {target_copy => $copy->id, checkin_time => undef}, $flesh
720             ])->[0];
721
722             if($circ) {
723                 $patron = $circ->usr;
724                 $circ->usr($patron->id); # de-flesh for consistency
725                 $self->circ($circ); 
726             }
727         }
728     }
729
730     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
731         unless $self->patron($patron) or $self->is_checkin;
732
733     unless($self->is_checkin) {
734
735         # Check for inactivity and patron reg. expiration
736
737         $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
738             unless $U->is_true($patron->active);
739     
740         $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
741             unless $U->is_true($patron->card->active);
742     
743         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
744             cleanse_ISO8601($patron->expire_date));
745     
746         $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
747             if( CORE::time > $expire->epoch ) ;
748     }
749 }
750
751
752 # --------------------------------------------------------------------------
753 # Does the circ permit work
754 # --------------------------------------------------------------------------
755 sub do_permit {
756     my $self = shift;
757
758     $self->log_me("do_permit()");
759
760     unless( $self->editor->requestor->id == $self->patron->id ) {
761         return $self->bail_on_events($self->editor->event)
762             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
763     }
764
765     $self->check_captured_holds();
766     $self->do_copy_checks();
767     return if $self->bail_out;
768     $self->run_patron_permit_scripts();
769     $self->run_copy_permit_scripts() 
770         unless $self->is_precat or $self->is_noncat;
771     $self->check_item_deposit_events();
772     $self->override_events();
773     return if $self->bail_out;
774
775     if($self->is_precat and not $self->request_precat) {
776         $self->push_events(
777             OpenILS::Event->new(
778                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
779         return $self->bail_out(1) unless $self->is_renewal;
780     }
781
782     $self->push_events(
783         OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
784 }
785
786 sub check_item_deposit_events {
787     my $self = shift;
788     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy)) 
789         if $self->is_deposit and not $self->is_deposit_exempt;
790     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy)) 
791         if $self->is_rental and not $self->is_rental_exempt;
792 }
793
794 # returns true if the user is not required to pay deposits
795 sub is_deposit_exempt {
796     my $self = shift;
797     my $pid = (ref $self->patron->profile) ?
798         $self->patron->profile->id : $self->patron->profile;
799     my $groups = $U->ou_ancestor_setting_value(
800         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
801     for my $grp (@$groups) {
802         return 1 if $self->is_group_descendant($grp, $pid);
803     }
804     return 0;
805 }
806
807 # returns true if the user is not required to pay rental fees
808 sub is_rental_exempt {
809     my $self = shift;
810     my $pid = (ref $self->patron->profile) ?
811         $self->patron->profile->id : $self->patron->profile;
812     my $groups = $U->ou_ancestor_setting_value(
813         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
814     for my $grp (@$groups) {
815         return 1 if $self->is_group_descendant($grp, $pid);
816     }
817     return 0;
818 }
819
820 sub is_group_descendant {
821     my($self, $p_id, $c_id) = @_;
822     return 0 unless defined $p_id and defined $c_id;
823     return 1 if $c_id == $p_id;
824     while(my $grp = $user_groups{$c_id}) {
825         $c_id = $grp->parent;
826         return 0 unless defined $c_id;
827         return 1 if $c_id == $p_id;
828     }
829     return 0;
830 }
831
832 sub check_captured_holds {
833     my $self    = shift;
834     my $copy    = $self->copy;
835     my $patron  = $self->patron;
836
837     return undef unless $copy;
838
839     my $s = $U->copy_status($copy->status)->id;
840     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
841     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
842
843     # Item is on the holds shelf, make sure it's going to the right person
844     my $hold = $self->editor->search_action_hold_request(
845         [
846             { 
847                 current_copy        => $copy->id , 
848                 capture_time        => { '!=' => undef },
849                 cancel_time         => undef, 
850                 fulfillment_time    => undef 
851             },
852             { limit => 1 }
853         ]
854     )->[0];
855
856     if ($hold and $hold->usr == $patron->id) {
857         $self->checkout_is_for_hold(1);
858         return undef;
859     }
860
861     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
862
863     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
864 }
865
866
867 sub do_copy_checks {
868     my $self = shift;
869     my $copy = $self->copy;
870     return unless $copy;
871
872     my $stat = $U->copy_status($copy->status)->id;
873
874     # We cannot check out a copy if it is in-transit
875     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
876         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
877     }
878
879     $self->handle_claims_returned();
880     return if $self->bail_out;
881
882     # no claims returned circ was found, check if there is any open circ
883     unless( $self->is_renewal ) {
884
885         my $circs = $self->editor->search_action_circulation(
886             { target_copy => $copy->id, checkin_time => undef }
887         );
888
889         if(my $old_circ = $circs->[0]) { # an open circ was found
890
891             my $payload = {copy => $copy};
892
893             if($old_circ->usr == $self->patron->id) {
894                 
895                 $payload->{old_circ} = $old_circ;
896
897                 # If there is an open circulation on the checkout item and an auto-renew 
898                 # interval is defined, inform the caller that they should go 
899                 # ahead and renew the item instead of warning about open circulations.
900     
901                 my $auto_renew_intvl = $U->ou_ancestor_setting_value(        
902                     $self->circ_lib,
903                     'circ.checkout_auto_renew_age', 
904                     $self->editor
905                 );
906
907                 if($auto_renew_intvl) {
908                     my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
909                     my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
910
911                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
912                         $payload->{auto_renew} = 1;
913                     }
914                 }
915             }
916
917             return $self->bail_on_events(
918                 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
919             );
920         }
921     }
922 }
923
924 my $LEGACY_CIRC_EVENT_MAP = {
925     'no_item' => 'ITEM_NOT_CATALOGED',
926     'actor.usr.barred' => 'PATRON_BARRED',
927     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
928     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
929     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
930     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
931     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
932     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
933     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
934     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
935     'config.circ_matrix_test.total_copy_hold_ratio' => 
936         'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
937     'config.circ_matrix_test.available_copy_hold_ratio' => 
938         'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
939 };
940
941
942 # ---------------------------------------------------------------------
943 # This pushes any patron-related events into the list but does not
944 # set bail_out for any events
945 # ---------------------------------------------------------------------
946 sub run_patron_permit_scripts {
947     my $self        = shift;
948     my $patronid    = $self->patron->id;
949
950     my @allevents; 
951
952
953     my $results = $self->run_indb_circ_test;
954     unless($self->circ_test_success) {
955         my @trimmed_results;
956
957         if ($self->is_noncat) {
958             # no_item result is OK during noncat checkout
959             @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
960
961         } else {
962
963             if ($self->checkout_is_for_hold) {
964                 # if this checkout will fulfill a hold, ignore CIRC blocks
965                 # and rely instead on the (later-checked) FULFILL block
966
967                 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
968                 my $fblock_pens = $self->editor->search_config_standing_penalty(
969                     {name => [@pen_names], block_list => {like => '%CIRC%'}});
970
971                 for my $res (@$results) {
972                     my $name = $res->{fail_part} || '';
973                     next if grep {$_->name eq $name} @$fblock_pens;
974                     push(@trimmed_results, $res);
975                 }
976
977             } else { 
978                 # not for hold or noncat
979                 @trimmed_results = @$results;
980             }
981         }
982
983         # update the final set of test results
984         $self->matrix_test_result(\@trimmed_results); 
985
986         push @allevents, $self->matrix_test_result_events;
987     }
988
989     for (@allevents) {
990        $_->{payload} = $self->copy if 
991              ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
992     }
993
994     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
995
996     $self->push_events(@allevents);
997 }
998
999 sub matrix_test_result_codes {
1000     my $self = shift;
1001     map { $_->{"fail_part"} } @{$self->matrix_test_result};
1002 }
1003
1004 sub matrix_test_result_events {
1005     my $self = shift;
1006     map {
1007         my $event = new OpenILS::Event(
1008             $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1009         );
1010         $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1011         $event;
1012     } (@{$self->matrix_test_result});
1013 }
1014
1015 sub run_indb_circ_test {
1016     my $self = shift;
1017     return $self->matrix_test_result if $self->matrix_test_result;
1018
1019     my $dbfunc = ($self->is_renewal) ? 
1020         'action.item_user_renew_test' : 'action.item_user_circ_test';
1021
1022     if( $self->is_precat && $self->request_precat) {
1023         $self->make_precat_copy;
1024         return if $self->bail_out;
1025     }
1026
1027     my $results = $self->editor->json_query(
1028         {   from => [
1029                 $dbfunc,
1030                 $self->circ_lib,
1031                 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id, 
1032                 $self->patron->id,
1033             ]
1034         }
1035     );
1036
1037     $self->circ_test_success($U->is_true($results->[0]->{success}));
1038
1039     if(my $mp = $results->[0]->{matchpoint}) {
1040         $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1041         $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1042         $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1043         if(defined($results->[0]->{renewals})) {
1044             $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1045         }
1046         $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1047         if(defined($results->[0]->{grace_period})) {
1048             $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1049         }
1050         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1051         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1052         # Grab the *last* response for limit_groups, where it is more likely to be filled
1053         $self->limit_groups($results->[-1]->{limit_groups});
1054     }
1055
1056     return $self->matrix_test_result($results);
1057 }
1058
1059 # ---------------------------------------------------------------------
1060 # given a use and copy, this will calculate the circulation policy
1061 # parameters.  Only works with in-db circ.
1062 # ---------------------------------------------------------------------
1063 sub do_inspect {
1064     my $self = shift;
1065
1066     return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1067
1068     $self->run_indb_circ_test;
1069
1070     my $results = {
1071         circ_test_success => $self->circ_test_success,
1072         failure_events => [],
1073         failure_codes => [],
1074         matchpoint => $self->circ_matrix_matchpoint
1075     };
1076
1077     unless($self->circ_test_success) {
1078         $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1079         $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1080     }
1081
1082     if($self->circ_matrix_matchpoint) {
1083         my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1084         my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1085         my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1086         my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1087     
1088         my $policy = $self->get_circ_policy(
1089             $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1090     
1091         $$results{$_} = $$policy{$_} for keys %$policy;
1092     }
1093
1094     return $results;
1095 }
1096
1097 # ---------------------------------------------------------------------
1098 # Loads the circ policy info for duration, recurring fine, and max
1099 # fine based on the current copy
1100 # ---------------------------------------------------------------------
1101 sub get_circ_policy {
1102     my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1103
1104     my $policy = {
1105         duration_rule => $duration_rule->name,
1106         recurring_fine_rule => $recurring_fine_rule->name,
1107         max_fine_rule => $max_fine_rule->name,
1108         max_fine => $self->get_max_fine_amount($max_fine_rule),
1109         fine_interval => $recurring_fine_rule->recurrence_interval,
1110         renewal_remaining => $duration_rule->max_renewals,
1111         grace_period => $recurring_fine_rule->grace_period
1112     };
1113
1114     if($hard_due_date) {
1115         $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1116         $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1117     }
1118     else {
1119         $policy->{duration_date_ceiling} = undef;
1120         $policy->{duration_date_ceiling_force} = undef;
1121     }
1122
1123     $policy->{duration} = $duration_rule->shrt
1124         if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1125     $policy->{duration} = $duration_rule->normal
1126         if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1127     $policy->{duration} = $duration_rule->extended
1128         if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1129
1130     $policy->{recurring_fine} = $recurring_fine_rule->low
1131         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1132     $policy->{recurring_fine} = $recurring_fine_rule->normal
1133         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1134     $policy->{recurring_fine} = $recurring_fine_rule->high
1135         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1136
1137     return $policy;
1138 }
1139
1140 sub get_max_fine_amount {
1141     my $self = shift;
1142     my $max_fine_rule = shift;
1143     my $max_amount = $max_fine_rule->amount;
1144
1145     # if is_percent is true then the max->amount is
1146     # use as a percentage of the copy price
1147     if ($U->is_true($max_fine_rule->is_percent)) {
1148         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1149         $max_amount = $price * $max_fine_rule->amount / 100;
1150     } elsif (
1151         $U->ou_ancestor_setting_value(
1152             $self->circ_lib,
1153             'circ.max_fine.cap_at_price',
1154             $self->editor
1155         )
1156     ) {
1157         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1158         $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1159     }
1160
1161     return $max_amount;
1162 }
1163
1164
1165
1166 sub run_copy_permit_scripts {
1167     my $self = shift;
1168     my $copy = $self->copy || return;
1169
1170     my @allevents;
1171
1172     my $results = $self->run_indb_circ_test;
1173     push @allevents, $self->matrix_test_result_events
1174         unless $self->circ_test_success;
1175
1176     # See if this copy has an alert message
1177     my $ae = $self->check_copy_alert();
1178     push( @allevents, $ae ) if $ae;
1179
1180     # uniquify the events
1181     my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1182     @allevents = values %hash;
1183
1184     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1185
1186     $self->push_events(@allevents);
1187 }
1188
1189
1190 sub check_copy_alert {
1191     my $self = shift;
1192     return undef if $self->is_renewal;
1193     return OpenILS::Event->new(
1194         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1195         if $self->copy and $self->copy->alert_message;
1196     return undef;
1197 }
1198
1199
1200
1201 # --------------------------------------------------------------------------
1202 # If the call is overriding and has permissions to override every collected
1203 # event, the are cleared.  Any event that the caller does not have
1204 # permission to override, will be left in the event list and bail_out will
1205 # be set
1206 # XXX We need code in here to cancel any holds/transits on copies 
1207 # that are being force-checked out
1208 # --------------------------------------------------------------------------
1209 sub override_events {
1210     my $self = shift;
1211     my @events = @{$self->events};
1212     return unless @events;
1213     my $oargs = $self->override_args;
1214
1215     if(!$self->override) {
1216         return $self->bail_out(1) 
1217             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1218     }   
1219
1220     $self->events([]);
1221     
1222     for my $e (@events) {
1223         my $tc = $e->{textcode};
1224         next if $tc eq 'SUCCESS';
1225         if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1226             my $ov = "$tc.override";
1227             $logger->info("circulator: attempting to override event: $ov");
1228
1229             return $self->bail_on_events($self->editor->event)
1230                 unless( $self->editor->allowed($ov) );
1231         } else {
1232             return $self->bail_out(1);
1233         }
1234    }
1235 }
1236     
1237
1238 # --------------------------------------------------------------------------
1239 # If there is an open claimsreturn circ on the requested copy, close the 
1240 # circ if overriding, otherwise bail out
1241 # --------------------------------------------------------------------------
1242 sub handle_claims_returned {
1243     my $self = shift;
1244     my $copy = $self->copy;
1245
1246     my $CR = $self->editor->search_action_circulation(
1247         {   
1248             target_copy     => $copy->id,
1249             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1250             checkin_time    => undef,
1251         }
1252     );
1253
1254     return unless ($CR = $CR->[0]); 
1255
1256     my $evt;
1257
1258     # - If the caller has set the override flag, we will check the item in
1259     if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1260
1261         $CR->checkin_time('now');   
1262         $CR->checkin_scan_time('now');   
1263         $CR->checkin_lib($self->circ_lib);
1264         $CR->checkin_workstation($self->editor->requestor->wsid);
1265         $CR->checkin_staff($self->editor->requestor->id);
1266
1267         $evt = $self->editor->event 
1268             unless $self->editor->update_action_circulation($CR);
1269
1270     } else {
1271         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1272     }
1273
1274     $self->bail_on_events($evt) if $evt;
1275     return;
1276 }
1277
1278
1279 # --------------------------------------------------------------------------
1280 # This performs the checkout
1281 # --------------------------------------------------------------------------
1282 sub do_checkout {
1283     my $self = shift;
1284
1285     $self->log_me("do_checkout()");
1286
1287     # make sure perms are good if this isn't a renewal
1288     unless( $self->is_renewal ) {
1289         return $self->bail_on_events($self->editor->event)
1290             unless( $self->editor->allowed('COPY_CHECKOUT') );
1291     }
1292
1293     # verify the permit key
1294     unless( $self->check_permit_key ) {
1295         if( $self->permit_override ) {
1296             return $self->bail_on_events($self->editor->event)
1297                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1298         } else {
1299             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1300         }   
1301     }
1302
1303     # if this is a non-cataloged circ, build the circ and finish
1304     if( $self->is_noncat ) {
1305         $self->checkout_noncat;
1306         $self->push_events(
1307             OpenILS::Event->new('SUCCESS', 
1308             payload => { noncat_circ => $self->circ }));
1309         return;
1310     }
1311
1312     if( $self->is_precat ) {
1313         $self->make_precat_copy;
1314         return if $self->bail_out;
1315
1316     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1317         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1318     }
1319
1320     $self->do_copy_checks;
1321     return if $self->bail_out;
1322
1323     $self->run_checkout_scripts();
1324     return if $self->bail_out;
1325
1326     $self->build_checkout_circ_object();
1327     return if $self->bail_out;
1328
1329     my $modify_to_start = $self->booking_adjusted_due_date();
1330     return if $self->bail_out;
1331
1332     $self->apply_modified_due_date($modify_to_start);
1333     return if $self->bail_out;
1334
1335     return $self->bail_on_events($self->editor->event)
1336         unless $self->editor->create_action_circulation($self->circ);
1337
1338     # refresh the circ to force local time zone for now
1339     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1340
1341     if($self->limit_groups) {
1342         $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1343     }
1344
1345     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1346     $self->update_copy;
1347     return if $self->bail_out;
1348
1349     $self->apply_deposit_fee();
1350     return if $self->bail_out;
1351
1352     $self->handle_checkout_holds();
1353     return if $self->bail_out;
1354
1355     # ------------------------------------------------------------------------------
1356     # Update the patron penalty info in the DB.  Run it for permit-overrides 
1357     # since the penalties are not updated during the permit phase
1358     # ------------------------------------------------------------------------------
1359     OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1360
1361     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1362     
1363     my $pcirc;
1364     if($self->is_renewal) {
1365         # flesh the billing summary for the checked-in circ
1366         $pcirc = $self->editor->retrieve_action_circulation([
1367             $self->parent_circ,
1368             {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1369         ]);
1370     }
1371
1372     $self->push_events(
1373         OpenILS::Event->new('SUCCESS',
1374             payload  => {
1375                 copy             => $U->unflesh_copy($self->copy),
1376                 volume           => $self->volume,
1377                 circ             => $self->circ,
1378                 record           => $record,
1379                 holds_fulfilled  => $self->fulfilled_holds,
1380                 deposit_billing  => $self->deposit_billing,
1381                 rental_billing   => $self->rental_billing,
1382                 parent_circ      => $pcirc,
1383                 patron           => ($self->return_patron) ? $self->patron : undef,
1384                 patron_money     => $self->editor->retrieve_money_user_summary($self->patron->id)
1385             }
1386         )
1387     );
1388 }
1389
1390 sub apply_deposit_fee {
1391     my $self = shift;
1392     my $copy = $self->copy;
1393     return unless 
1394         ($self->is_deposit and not $self->is_deposit_exempt) or 
1395         ($self->is_rental and not $self->is_rental_exempt);
1396
1397     return if $self->is_deposit and $self->skip_deposit_fee;
1398     return if $self->is_rental and $self->skip_rental_fee;
1399
1400     my $bill = Fieldmapper::money::billing->new;
1401     my $amount = $copy->deposit_amount;
1402     my $billing_type;
1403     my $btype;
1404
1405     if($self->is_deposit) {
1406         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1407         $btype = 5;
1408         $self->deposit_billing($bill);
1409     } else {
1410         $billing_type = OILS_BILLING_TYPE_RENTAL;
1411         $btype = 6;
1412         $self->rental_billing($bill);
1413     }
1414
1415     $bill->xact($self->circ->id);
1416     $bill->amount($amount);
1417     $bill->note(OILS_BILLING_NOTE_SYSTEM);
1418     $bill->billing_type($billing_type);
1419     $bill->btype($btype);
1420     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1421
1422     $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1423 }
1424
1425 sub update_copy {
1426     my $self = shift;
1427     my $copy = $self->copy;
1428
1429     my $stat = $copy->status if ref $copy->status;
1430     my $loc = $copy->location if ref $copy->location;
1431     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1432
1433     $copy->status($stat->id) if $stat;
1434     $copy->location($loc->id) if $loc;
1435     $copy->circ_lib($circ_lib->id) if $circ_lib;
1436     $copy->editor($self->editor->requestor->id);
1437     $copy->edit_date('now');
1438     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1439
1440     return $self->bail_on_events($self->editor->event)
1441         unless $self->editor->update_asset_copy($self->copy);
1442
1443     $copy->status($U->copy_status($copy->status));
1444     $copy->location($loc) if $loc;
1445     $copy->circ_lib($circ_lib) if $circ_lib;
1446 }
1447
1448 sub update_reservation {
1449     my $self = shift;
1450     my $reservation = $self->reservation;
1451
1452     my $usr = $reservation->usr;
1453     my $target_rt = $reservation->target_resource_type;
1454     my $target_r = $reservation->target_resource;
1455     my $current_r = $reservation->current_resource;
1456
1457     $reservation->usr($usr->id) if ref $usr;
1458     $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1459     $reservation->target_resource($target_r->id) if ref $target_r;
1460     $reservation->current_resource($current_r->id) if ref $current_r;
1461
1462     return $self->bail_on_events($self->editor->event)
1463         unless $self->editor->update_booking_reservation($self->reservation);
1464
1465     my $evt;
1466     ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1467     $self->reservation($reservation);
1468 }
1469
1470
1471 sub bail_on_events {
1472     my( $self, @evts ) = @_;
1473     $self->push_events(@evts);
1474     $self->bail_out(1);
1475 }
1476
1477 # ------------------------------------------------------------------------------
1478 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1479 # affects copies that will fulfill holds and CIRC affects all other copies.
1480 # If blocks exists, bail, push Events onto the event pile, and return true.
1481 # ------------------------------------------------------------------------------
1482 sub check_hold_fulfill_blocks {
1483     my $self = shift;
1484
1485     # See if the user has any penalties applied that prevent hold fulfillment
1486     my $pens = $self->editor->json_query({
1487         select => {csp => ['name', 'label']},
1488         from => {ausp => {csp => {}}},
1489         where => {
1490             '+ausp' => {
1491                 usr => $self->patron->id,
1492                 org_unit => $U->get_org_full_path($self->circ_lib),
1493                 '-or' => [
1494                     {stop_date => undef},
1495                     {stop_date => {'>' => 'now'}}
1496                 ]
1497             },
1498             '+csp' => {block_list => {'like' => '%FULFILL%'}}
1499         }
1500     });
1501
1502     return 0 unless @$pens;
1503
1504     for my $pen (@$pens) {
1505         $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1506         my $event = OpenILS::Event->new($pen->{name});
1507         $event->{desc} = $pen->{label};
1508         $self->push_events($event);
1509     }
1510
1511     $self->override_events;
1512     return $self->bail_out;
1513 }
1514
1515
1516 # ------------------------------------------------------------------------------
1517 # When an item is checked out, see if we can fulfill a hold for this patron
1518 # ------------------------------------------------------------------------------
1519 sub handle_checkout_holds {
1520    my $self    = shift;
1521    my $copy    = $self->copy;
1522    my $patron  = $self->patron;
1523
1524    my $e = $self->editor;
1525    $self->fulfilled_holds([]);
1526
1527    # non-cats can't fulfill a hold
1528    return if $self->is_noncat;
1529
1530     my $hold = $e->search_action_hold_request({   
1531         current_copy        => $copy->id , 
1532         cancel_time         => undef, 
1533         fulfillment_time    => undef,
1534         '-or' => [
1535             {expire_time => undef},
1536             {expire_time => {'>' => 'now'}}
1537         ]
1538     })->[0];
1539
1540     if($hold and $hold->usr != $patron->id) {
1541         # reset the hold since the copy is now checked out
1542     
1543         $logger->info("circulator: un-targeting hold ".$hold->id.
1544             " because copy ".$copy->id." is getting checked out");
1545
1546         $hold->clear_prev_check_time; 
1547         $hold->clear_current_copy;
1548         $hold->clear_capture_time;
1549         $hold->clear_shelf_time;
1550         $hold->clear_shelf_expire_time;
1551         $hold->clear_current_shelf_lib;
1552
1553         return $self->bail_on_event($e->event)
1554             unless $e->update_action_hold_request($hold);
1555
1556         $hold = undef;
1557     }
1558
1559     unless($hold) {
1560         $hold = $self->find_related_user_hold($copy, $patron) or return;
1561         $logger->info("circulator: found related hold to fulfill in checkout");
1562     }
1563
1564     return if $self->check_hold_fulfill_blocks;
1565
1566     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1567
1568     # if the hold was never officially captured, capture it.
1569     $hold->current_copy($copy->id);
1570     $hold->capture_time('now') unless $hold->capture_time;
1571     $hold->fulfillment_time('now');
1572     $hold->fulfillment_staff($e->requestor->id);
1573     $hold->fulfillment_lib($self->circ_lib);
1574
1575     return $self->bail_on_events($e->event)
1576         unless $e->update_action_hold_request($hold);
1577
1578     return $self->fulfilled_holds([$hold->id]);
1579 }
1580
1581
1582 # ------------------------------------------------------------------------------
1583 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1584 # the patron directly targets the checked out item, see if there is another hold 
1585 # for the patron that could be fulfilled by the checked out item.  Fulfill the
1586 # oldest hold and only fulfill 1 of them.
1587
1588 # For "another hold":
1589 #
1590 # First, check for one that the copy matches via hold_copy_map, ensuring that
1591 # *any* hold type that this copy could fill may end up filled.
1592 #
1593 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1594 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1595 # that are non-requestable to count as capturing those hold types.
1596 # ------------------------------------------------------------------------------
1597 sub find_related_user_hold {
1598     my($self, $copy, $patron) = @_;
1599     my $e = $self->editor;
1600
1601     # holds on precat copies are always copy-level, so this call will
1602     # always return undef.  Exit early.
1603     return undef if $self->is_precat;
1604
1605     return undef unless $U->ou_ancestor_setting_value(        
1606         $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1607
1608     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1609     my $args = {
1610         select => {ahr => ['id']}, 
1611         from => {
1612             ahr => {
1613                 ahcm => {
1614                     field => 'hold',
1615                     fkey => 'id'
1616                 },
1617                 acp => {
1618                     field => 'id', 
1619                     fkey => 'current_copy',
1620                     type => 'left' # there may be no current_copy
1621                 }
1622             }
1623         }, 
1624         where => {
1625             '+ahr' => {
1626                 usr => $patron->id,
1627                 fulfillment_time => undef,
1628                 cancel_time => undef,
1629                '-or' => [
1630                     {expire_time => undef},
1631                     {expire_time => {'>' => 'now'}}
1632                 ]
1633             },
1634             '+ahcm' => {
1635                 target_copy => $self->copy->id
1636             },
1637             '+acp' => {
1638                 '-or' => [
1639                     {id => undef}, # left-join copy may be nonexistent
1640                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1641                 ]
1642             }
1643         },
1644         order_by => {ahr => {request_time => {direction => 'asc'}}},
1645         limit => 1
1646     };
1647
1648     my $hold_info = $e->json_query($args)->[0];
1649     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1650     return undef if $U->ou_ancestor_setting_value(        
1651         $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1652
1653     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1654     $args = {
1655         select => {ahr => ['id']}, 
1656         from => {
1657             ahr => {
1658                 acp => {
1659                     field => 'id', 
1660                     fkey => 'current_copy',
1661                     type => 'left' # there may be no current_copy
1662                 }
1663             }
1664         }, 
1665         where => {
1666             '+ahr' => {
1667                 usr => $patron->id,
1668                 fulfillment_time => undef,
1669                 cancel_time => undef,
1670                '-or' => [
1671                     {expire_time => undef},
1672                     {expire_time => {'>' => 'now'}}
1673                 ]
1674             },
1675             '-or' => [
1676                 {
1677                     '+ahr' => { 
1678                         hold_type => 'V',
1679                         target => $self->volume->id
1680                     }
1681                 },
1682                 { 
1683                     '+ahr' => { 
1684                         hold_type => 'T',
1685                         target => $self->title->id
1686                     }
1687                 },
1688             ],
1689             '+acp' => {
1690                 '-or' => [
1691                     {id => undef}, # left-join copy may be nonexistent
1692                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1693                 ]
1694             }
1695         },
1696         order_by => {ahr => {request_time => {direction => 'asc'}}},
1697         limit => 1
1698     };
1699
1700     $hold_info = $e->json_query($args)->[0];
1701     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1702     return undef;
1703 }
1704
1705
1706 sub run_checkout_scripts {
1707     my $self = shift;
1708     my $nobail = shift;
1709
1710     my $evt;
1711
1712     my $duration;
1713     my $recurring;
1714     my $max_fine;
1715     my $hard_due_date;
1716     my $duration_name;
1717     my $recurring_name;
1718     my $max_fine_name;
1719     my $hard_due_date_name;
1720
1721     $self->run_indb_circ_test();
1722     $duration = $self->circ_matrix_matchpoint->duration_rule;
1723     $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1724     $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1725     $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1726
1727     $duration_name = $duration->name if $duration;
1728     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1729
1730         unless($duration) {
1731             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1732             return $self->bail_on_events($evt) if ($evt && !$nobail);
1733         
1734             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1735             return $self->bail_on_events($evt) if ($evt && !$nobail);
1736         
1737             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1738             return $self->bail_on_events($evt) if ($evt && !$nobail);
1739
1740             if($hard_due_date_name) {
1741                 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1742                 return $self->bail_on_events($evt) if ($evt && !$nobail);
1743             }
1744         }
1745
1746     } else {
1747
1748         # The item circulates with an unlimited duration
1749         $duration   = undef;
1750         $recurring  = undef;
1751         $max_fine   = undef;
1752         $hard_due_date = undef;
1753     }
1754
1755    $self->duration_rule($duration);
1756    $self->recurring_fines_rule($recurring);
1757    $self->max_fine_rule($max_fine);
1758    $self->hard_due_date($hard_due_date);
1759 }
1760
1761
1762 sub build_checkout_circ_object {
1763     my $self = shift;
1764
1765    my $circ       = Fieldmapper::action::circulation->new;
1766    my $duration   = $self->duration_rule;
1767    my $max        = $self->max_fine_rule;
1768    my $recurring  = $self->recurring_fines_rule;
1769    my $hard_due_date    = $self->hard_due_date;
1770    my $copy       = $self->copy;
1771    my $patron     = $self->patron;
1772    my $duration_date_ceiling;
1773    my $duration_date_ceiling_force;
1774
1775     if( $duration ) {
1776
1777         my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1778         $duration_date_ceiling = $policy->{duration_date_ceiling};
1779         $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1780
1781         my $dname = $duration->name;
1782         my $mname = $max->name;
1783         my $rname = $recurring->name;
1784         my $hdname = ''; 
1785         if($hard_due_date) {
1786             $hdname = $hard_due_date->name;
1787         }
1788
1789         $logger->debug("circulator: building circulation ".
1790             "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1791     
1792         $circ->duration($policy->{duration});
1793         $circ->recurring_fine($policy->{recurring_fine});
1794         $circ->duration_rule($duration->name);
1795         $circ->recurring_fine_rule($recurring->name);
1796         $circ->max_fine_rule($max->name);
1797         $circ->max_fine($policy->{max_fine});
1798         $circ->fine_interval($recurring->recurrence_interval);
1799         $circ->renewal_remaining($duration->max_renewals);
1800         $circ->grace_period($policy->{grace_period});
1801
1802     } else {
1803
1804         $logger->info("circulator: copy found with an unlimited circ duration");
1805         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1806         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1807         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1808         $circ->renewal_remaining(0);
1809         $circ->grace_period(0);
1810     }
1811
1812    $circ->target_copy( $copy->id );
1813    $circ->usr( $patron->id );
1814    $circ->circ_lib( $self->circ_lib );
1815    $circ->workstation($self->editor->requestor->wsid) 
1816     if defined $self->editor->requestor->wsid;
1817
1818     # renewals maintain a link to the parent circulation
1819     $circ->parent_circ($self->parent_circ);
1820
1821    if( $self->is_renewal ) {
1822       $circ->opac_renewal('t') if $self->opac_renewal;
1823       $circ->phone_renewal('t') if $self->phone_renewal;
1824       $circ->desk_renewal('t') if $self->desk_renewal;
1825       $circ->renewal_remaining($self->renewal_remaining);
1826       $circ->circ_staff($self->editor->requestor->id);
1827    }
1828
1829
1830     # if the user provided an overiding checkout time,
1831     # (e.g. the checkout really happened several hours ago), then
1832     # we apply that here.  Does this need a perm??
1833     $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1834         if $self->checkout_time;
1835
1836     # if a patron is renewing, 'requestor' will be the patron
1837     $circ->circ_staff($self->editor->requestor->id);
1838     $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
1839
1840     $self->circ($circ);
1841 }
1842
1843 sub do_reservation_pickup {
1844     my $self = shift;
1845
1846     $self->log_me("do_reservation_pickup()");
1847
1848     $self->reservation->pickup_time('now');
1849
1850     if (
1851         $self->reservation->current_resource &&
1852         $U->is_true($self->reservation->target_resource_type->catalog_item)
1853     ) {
1854         # We used to try to set $self->copy and $self->patron here,
1855         # but that should already be done.
1856
1857         $self->run_checkout_scripts(1);
1858
1859         my $duration   = $self->duration_rule;
1860         my $max        = $self->max_fine_rule;
1861         my $recurring  = $self->recurring_fines_rule;
1862
1863         if ($duration && $max && $recurring) {
1864             my $policy = $self->get_circ_policy($duration, $recurring, $max);
1865
1866             my $dname = $duration->name;
1867             my $mname = $max->name;
1868             my $rname = $recurring->name;
1869
1870             $logger->debug("circulator: updating reservation ".
1871                 "with duration=$dname, maxfine=$mname, recurring=$rname");
1872
1873             $self->reservation->fine_amount($policy->{recurring_fine});
1874             $self->reservation->max_fine($policy->{max_fine});
1875             $self->reservation->fine_interval($recurring->recurrence_interval);
1876         }
1877
1878         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1879         $self->update_copy();
1880
1881     } else {
1882         $self->reservation->fine_amount(
1883             $self->reservation->target_resource_type->fine_amount
1884         );
1885         $self->reservation->max_fine(
1886             $self->reservation->target_resource_type->max_fine
1887         );
1888         $self->reservation->fine_interval(
1889             $self->reservation->target_resource_type->fine_interval
1890         );
1891     }
1892
1893     $self->update_reservation();
1894 }
1895
1896 sub do_reservation_return {
1897     my $self = shift;
1898     my $request = shift;
1899
1900     $self->log_me("do_reservation_return()");
1901
1902     if (not ref $self->reservation) {
1903         my ($reservation, $evt) =
1904             $U->fetch_booking_reservation($self->reservation);
1905         return $self->bail_on_events($evt) if $evt;
1906         $self->reservation($reservation);
1907     }
1908
1909     $self->handle_fines(1);
1910     $self->reservation->return_time('now');
1911     $self->update_reservation();
1912     $self->reshelve_copy if $self->copy;
1913
1914     if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1915         $self->copy( $self->reservation->current_resource->catalog_item );
1916     }
1917 }
1918
1919 sub booking_adjusted_due_date {
1920     my $self = shift;
1921     my $circ = $self->circ;
1922     my $copy = $self->copy;
1923
1924     return undef unless $self->use_booking;
1925
1926     my $changed;
1927
1928     if( $self->due_date ) {
1929
1930         return $self->bail_on_events($self->editor->event)
1931             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1932
1933        $circ->due_date(cleanse_ISO8601($self->due_date));
1934
1935     } else {
1936
1937         return unless $copy and $circ->due_date;
1938     }
1939
1940     my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1941     if (@$booking_items) {
1942         my $booking_item = $booking_items->[0];
1943         my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1944
1945         my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1946         my $shorten_circ_setting = $resource_type->elbow_room ||
1947             $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1948             '0 seconds';
1949
1950         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1951         my $bookings = $booking_ses->request(
1952             'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1953             { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
1954         )->gather(1);
1955         $booking_ses->disconnect;
1956         
1957         my $dt_parser = DateTime::Format::ISO8601->new;
1958         my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1959
1960         for my $bid (@$bookings) {
1961
1962             my $booking = $self->editor->retrieve_booking_reservation( $bid );
1963
1964             my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1965             my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1966
1967             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1968                 if ($booking_start < DateTime->now);
1969
1970
1971             if ($U->is_true($stop_circ_setting)) {
1972                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
1973             } else {
1974                 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
1975                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
1976             }
1977             
1978             # We set the circ duration here only to affect the logic that will
1979             # later (in a DB trigger) mangle the time part of the due date to
1980             # 11:59pm. Having any circ duration that is not a whole number of
1981             # days is enough to prevent the "correction."
1982             my $new_circ_duration = $due_date->epoch - time;
1983             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
1984             $circ->duration("$new_circ_duration seconds");
1985
1986             $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
1987             $changed = 1;
1988         }
1989
1990         return $self->bail_on_events($self->editor->event)
1991             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1992     }
1993
1994     return $changed;
1995 }
1996
1997 sub apply_modified_due_date {
1998     my $self = shift;
1999     my $shift_earlier = shift;
2000     my $circ = $self->circ;
2001     my $copy = $self->copy;
2002
2003    if( $self->due_date ) {
2004
2005         return $self->bail_on_events($self->editor->event)
2006             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2007
2008       $circ->due_date(cleanse_ISO8601($self->due_date));
2009
2010    } else {
2011
2012       # if the due_date lands on a day when the location is closed
2013       return unless $copy and $circ->due_date;
2014
2015         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2016
2017         # due-date overlap should be determined by the location the item
2018         # is checked out from, not the owning or circ lib of the item
2019         my $org = $self->circ_lib;
2020
2021       $logger->info("circulator: circ searching for closed date overlap on lib $org".
2022             " with an item due date of ".$circ->due_date );
2023
2024       my $dateinfo = $U->storagereq(
2025          'open-ils.storage.actor.org_unit.closed_date.overlap', 
2026             $org, $circ->due_date );
2027
2028       if($dateinfo) {
2029          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2030             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2031
2032             # XXX make the behavior more dynamic
2033             # for now, we just push the due date to after the close date
2034             if ($shift_earlier) {
2035                 $circ->due_date($dateinfo->{start});
2036             } else {
2037                 $circ->due_date($dateinfo->{end});
2038             }
2039       }
2040    }
2041 }
2042
2043
2044
2045 sub create_due_date {
2046     my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2047
2048     # if there is a raw time component (e.g. from postgres), 
2049     # turn it into an interval that interval_to_seconds can parse
2050     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2051
2052     # for now, use the server timezone.  TODO: use workstation org timezone
2053     my $due_date = DateTime->now(time_zone => 'local');
2054     $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2055
2056     # add the circ duration
2057     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2058
2059     if($date_ceiling) {
2060         my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2061         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2062             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2063             $due_date = $cdate;
2064         }
2065     }
2066
2067     # return ISO8601 time with timezone
2068     return $due_date->strftime('%FT%T%z');
2069 }
2070
2071
2072
2073 sub make_precat_copy {
2074     my $self = shift;
2075     my $copy = $self->copy;
2076
2077    if($copy) {
2078         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2079
2080         $copy->editor($self->editor->requestor->id);
2081         $copy->edit_date('now');
2082         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2083         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2084         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2085         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2086         $self->update_copy();
2087         return;
2088    }
2089
2090     $logger->info("circulator: Creating a new precataloged ".
2091         "copy in checkout with barcode " . $self->copy_barcode);
2092
2093     $copy = Fieldmapper::asset::copy->new;
2094     $copy->circ_lib($self->circ_lib);
2095     $copy->creator($self->editor->requestor->id);
2096     $copy->editor($self->editor->requestor->id);
2097     $copy->barcode($self->copy_barcode);
2098     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
2099     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2100     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2101
2102     $copy->dummy_title($self->dummy_title || "");
2103     $copy->dummy_author($self->dummy_author || "");
2104     $copy->dummy_isbn($self->dummy_isbn || "");
2105     $copy->circ_modifier($self->circ_modifier);
2106
2107
2108     # See if we need to override the circ_lib for the copy with a configured circ_lib
2109     # Setting is shortname of the org unit
2110     my $precat_circ_lib = $U->ou_ancestor_setting_value(
2111         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2112
2113     if($precat_circ_lib) {
2114         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2115
2116         if(!$org) {
2117             $self->bail_on_events($self->editor->event);
2118             return;
2119         }
2120
2121         $copy->circ_lib($org->id);
2122     }
2123
2124
2125     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2126         $self->bail_out(1);
2127         $self->push_events($self->editor->event);
2128         return;
2129     }   
2130 }
2131
2132
2133 sub checkout_noncat {
2134     my $self = shift;
2135
2136     my $circ;
2137     my $evt;
2138
2139    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
2140    my $count    = $self->noncat_count || 1;
2141    my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
2142
2143    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2144
2145    for(1..$count) {
2146
2147       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2148          $self->editor->requestor->id, 
2149             $self->patron->id, 
2150             $lib, 
2151             $self->noncat_type, 
2152             $cotime,
2153             $self->editor );
2154
2155         if( $evt ) {
2156             $self->push_events($evt);
2157             $self->bail_out(1);
2158             return; 
2159         }
2160         $self->circ($circ);
2161    }
2162 }
2163
2164 # If a copy goes into transit and is then checked in before the transit checkin 
2165 # interval has expired, push an event onto the overridable events list.
2166 sub check_transit_checkin_interval {
2167     my $self = shift;
2168
2169     # only concerned with in-transit items
2170     return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2171
2172     # no interval, no problem
2173     my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2174     return unless $interval;
2175
2176     # capture the transit so we don't have to fetch it again later during checkin
2177     $self->transit(
2178         $self->editor->search_action_transit_copy(
2179             {target_copy => $self->copy->id, dest_recv_time => undef}
2180         )->[0]
2181     ); 
2182
2183     # transit from X to X for whatever reason has no min interval
2184     return if $self->transit->source == $self->transit->dest;
2185
2186     my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2187     my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2188     my $horizon = $t_start->add(seconds => $seconds);
2189
2190     # See if we are still within the transit checkin forbidden range
2191     $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK')) 
2192         if $horizon > DateTime->now;
2193 }
2194
2195 # Retarget local holds at checkin
2196 sub checkin_retarget {
2197     my $self = shift;
2198     return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2199     return unless $self->is_checkin; # Renewals need not be checked
2200     return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2201     return if $self->is_precat; # No holds for precats
2202     return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2203     return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2204     my $status = $U->copy_status($self->copy->status);
2205     return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2206     # Specifically target items that are likely new (by status ID)
2207     return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2208     my $location = $self->copy->location;
2209     if(!ref($location)) {
2210         $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2211         $self->copy->location($location);
2212     }
2213     return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2214
2215     # Fetch holds for the bib
2216     my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2217                     $self->editor->authtoken,
2218                     $self->title->id,
2219                     {
2220                         capture_time => undef, # No touching captured holds
2221                         frozen => 'f', # Don't bother with frozen holds
2222                         pickup_lib => $self->circ_lib # Only holds actually here
2223                     }); 
2224
2225     # Error? Skip the step.
2226     return if exists $result->{"ilsevent"};
2227
2228     # Assemble holds
2229     my $holds = [];
2230     foreach my $holdlist (keys %{$result}) {
2231         push @$holds, @{$result->{$holdlist}};
2232     }
2233
2234     return if scalar(@$holds) == 0; # No holds, no retargeting
2235
2236     # Check for parts on this copy
2237     my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2238     my %parts_hash = ();
2239     %parts_hash = map {$_->part, 1} @$parts if @$parts;
2240
2241     # Loop over holds in request-ish order
2242     # Stage 1: Get them into request-ish order
2243     # Also grab type and target for skipping low hanging ones
2244     $result = $self->editor->json_query({
2245         "select" => { "ahr" => ["id", "hold_type", "target"] },
2246         "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
2247         "where" => { "id" => $holds },
2248         "order_by" => [
2249             { "class" => "pgt", "field" => "hold_priority"},
2250             { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2251             { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2252             { "class" => "ahr", "field" => "request_time"}
2253         ]
2254     });
2255
2256     # Stage 2: Loop!
2257     if (ref $result eq "ARRAY" and scalar @$result) {
2258         foreach (@{$result}) {
2259             # Copy level, but not this copy?
2260             next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2261                 and $_->{target} != $self->copy->id);
2262             # Volume level, but not this volume?
2263             next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2264             if(@$parts) { # We have parts?
2265                 # Skip title holds
2266                 next if ($_->{hold_type} eq 'T');
2267                 # Skip part holds for parts not on this copy
2268                 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2269             } else {
2270                 # No parts, no part holds
2271                 next if ($_->{hold_type} eq 'P');
2272             }
2273             # So much for easy stuff, attempt a retarget!
2274             my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2275             if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2276                 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2277             }
2278         }
2279     }
2280 }
2281
2282 sub do_checkin {
2283     my $self = shift;
2284     $self->log_me("do_checkin()");
2285
2286     return $self->bail_on_events(
2287         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2288         unless $self->copy;
2289
2290     $self->check_transit_checkin_interval;
2291     $self->checkin_retarget;
2292
2293     # the renew code and mk_env should have already found our circulation object
2294     unless( $self->circ ) {
2295
2296         my $circs = $self->editor->search_action_circulation(
2297             { target_copy => $self->copy->id, checkin_time => undef });
2298
2299         $self->circ($$circs[0]);
2300
2301         # for now, just warn if there are multiple open circs on a copy
2302         $logger->warn("circulator: we have ".scalar(@$circs).
2303             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2304     }
2305
2306     my $stat = $U->copy_status($self->copy->status)->id;
2307
2308     # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2309     # differently if they are already paid for.  We need to check for this
2310     # early since overdue generation is potentially affected.
2311     my $dont_change_lost_zero = 0;
2312     if ($stat == OILS_COPY_STATUS_LOST
2313         || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2314         || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2315
2316         # LOST fine settings are controlled by the copy's circ lib, not the the
2317         # circulation's
2318         my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2319                 $self->copy->circ_lib->id : $self->copy->circ_lib;
2320         $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2321             $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2322             $self->editor) || 0;
2323
2324         if ($dont_change_lost_zero) {
2325             my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2326             $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2327         }
2328
2329         $self->dont_change_lost_zero($dont_change_lost_zero);
2330     }
2331
2332     if( $self->checkin_check_holds_shelf() ) {
2333         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2334         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2335         if($self->fake_hold_dest) {
2336             $self->hold->pickup_lib($self->circ_lib);
2337         }
2338         $self->checkin_flesh_events;
2339         return;
2340     }
2341
2342     unless( $self->is_renewal ) {
2343         return $self->bail_on_events($self->editor->event)
2344             unless $self->editor->allowed('COPY_CHECKIN');
2345     }
2346
2347     $self->push_events($self->check_copy_alert());
2348     $self->push_events($self->check_checkin_copy_status());
2349
2350     # if the circ is marked as 'claims returned', add the event to the list
2351     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2352         if ($self->circ and $self->circ->stop_fines 
2353                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2354
2355     $self->check_circ_deposit();
2356
2357     # handle the overridable events 
2358     $self->override_events unless $self->is_renewal;
2359     return if $self->bail_out;
2360     
2361     if( $self->copy and !$self->transit ) {
2362         $self->transit(
2363             $self->editor->search_action_transit_copy(
2364                 { target_copy => $self->copy->id, dest_recv_time => undef }
2365             )->[0]
2366         ); 
2367     }
2368
2369     if( $self->circ ) {
2370         $self->checkin_handle_circ_start;
2371         return if $self->bail_out;
2372
2373         if (!$dont_change_lost_zero) {
2374             # if this circ is LOST and we are configured to generate overdue
2375             # fines for lost items on checkin (to fill the gap between mark
2376             # lost time and when the fines would have naturally stopped), then
2377             # stop_fines is no longer valid and should be cleared.
2378             #
2379             # stop_fines will be set again during the handle_fines() stage.
2380             # XXX should this setting come from the copy circ lib (like other
2381             # LOST settings), instead of the circulation circ lib?
2382             if ($stat == OILS_COPY_STATUS_LOST) {
2383                 $self->circ->clear_stop_fines if
2384                     $U->ou_ancestor_setting_value(
2385                         $self->circ_lib,
2386                         OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2387                         $self->editor
2388                     );
2389             }
2390
2391             # handle fines for this circ, including overdue gen if needed
2392             $self->handle_fines;
2393         }
2394
2395         $self->checkin_handle_circ_finish;
2396         return if $self->bail_out;
2397         $self->checkin_changed(1);
2398
2399     } elsif( $self->transit ) {
2400         my $hold_transit = $self->process_received_transit;
2401         $self->checkin_changed(1);
2402
2403         if( $self->bail_out ) { 
2404             $self->checkin_flesh_events;
2405             return;
2406         }
2407         
2408         if( my $e = $self->check_checkin_copy_status() ) {
2409             # If the original copy status is special, alert the caller
2410             my $ev = $self->events;
2411             $self->events([$e]);
2412             $self->override_events;
2413             return if $self->bail_out;
2414             $self->events($ev);
2415         }
2416
2417         if( $hold_transit or 
2418                 $U->copy_status($self->copy->status)->id 
2419                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2420
2421             my $hold;
2422             if( $hold_transit ) {
2423                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2424             } else {
2425                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2426             }
2427
2428             $self->hold($hold);
2429
2430             if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2431
2432                 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2433                 $self->reshelve_copy(1);
2434                 $self->cancelled_hold_transit(1);
2435                 $self->notify_hold(0); # don't notify for cancelled holds
2436                 $self->fake_hold_dest(0);
2437                 return if $self->bail_out;
2438
2439             } elsif ($hold and $hold->hold_type eq 'R') {
2440
2441                 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2442                 $self->notify_hold(0); # No need to notify
2443                 $self->fake_hold_dest(0);
2444                 $self->noop(1); # Don't try and capture for other holds/transits now
2445                 $self->update_copy();
2446                 $hold->fulfillment_time('now');
2447                 $self->bail_on_events($self->editor->event)
2448                     unless $self->editor->update_action_hold_request($hold);
2449
2450             } else {
2451
2452                 # hold transited to correct location
2453                 if($self->fake_hold_dest) {
2454                     $hold->pickup_lib($self->circ_lib);
2455                 }
2456                 $self->checkin_flesh_events;
2457                 return;
2458             }
2459         } 
2460
2461     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2462
2463         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2464             " that is in-transit, but there is no transit.. repairing");
2465         $self->reshelve_copy(1);
2466         return if $self->bail_out;
2467     }
2468
2469     if( $self->is_renewal ) {
2470         $self->finish_fines_and_voiding;
2471         return if $self->bail_out;
2472         $self->push_events(OpenILS::Event->new('SUCCESS'));
2473         return;
2474     }
2475
2476    # ------------------------------------------------------------------------------
2477    # Circulations and transits are now closed where necessary.  Now go on to see if
2478    # this copy can fulfill a hold or needs to be routed to a different location
2479    # ------------------------------------------------------------------------------
2480
2481     my $needed_for_something = 0; # formerly "needed_for_hold"
2482
2483     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2484
2485         if (!$self->remote_hold) {
2486             if ($self->use_booking) {
2487                 my $potential_hold = $self->hold_capture_is_possible;
2488                 my $potential_reservation = $self->reservation_capture_is_possible;
2489
2490                 if ($potential_hold and $potential_reservation) {
2491                     $logger->info("circulator: item could fulfill either hold or reservation");
2492                     $self->push_events(new OpenILS::Event(
2493                         "HOLD_RESERVATION_CONFLICT",
2494                         "hold" => $potential_hold,
2495                         "reservation" => $potential_reservation
2496                     ));
2497                     return if $self->bail_out;
2498                 } elsif ($potential_hold) {
2499                     $needed_for_something =
2500                         $self->attempt_checkin_hold_capture;
2501                 } elsif ($potential_reservation) {
2502                     $needed_for_something =
2503                         $self->attempt_checkin_reservation_capture;
2504                 }
2505             } else {
2506                 $needed_for_something = $self->attempt_checkin_hold_capture;
2507             }
2508         }
2509         return if $self->bail_out;
2510     
2511         unless($needed_for_something) {
2512             my $circ_lib = (ref $self->copy->circ_lib) ? 
2513                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2514     
2515             if( $self->remote_hold ) {
2516                 $circ_lib = $self->remote_hold->pickup_lib;
2517                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2518                     " is on a remote hold's shelf, sending to $circ_lib");
2519             }
2520     
2521             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2522
2523             my $suppress_transit = 0;
2524
2525             if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2526                 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2527                 if($suppress_transit_source && $suppress_transit_source->{value}) {
2528                     my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2529                     if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2530                         $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2531                         $suppress_transit = 1;
2532                     }
2533                 }
2534             }
2535  
2536             if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2537                 # copy is where it needs to be, either for hold or reshelving
2538     
2539                 $self->checkin_handle_precat();
2540                 return if $self->bail_out;
2541     
2542             } else {
2543                 # copy needs to transit "home", or stick here if it's a floating copy
2544                 my $can_float = 0;
2545                 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2546                     my $res = $self->editor->json_query(
2547                         {   from => [
2548                                 'evergreen.can_float',
2549                                 $self->copy->floating->id,
2550                                 $self->copy->circ_lib,
2551                                 $self->circ_lib
2552                             ]
2553                         }
2554                     );
2555                     $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res; 
2556                 }
2557                 if ($can_float) { # Yep, floating, stick here
2558                     $self->checkin_changed(1);
2559                     $self->copy->circ_lib( $self->circ_lib );
2560                     $self->update_copy;
2561                 } else {
2562                     my $bc = $self->copy->barcode;
2563                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2564                     $self->checkin_build_copy_transit($circ_lib);
2565                     return if $self->bail_out;
2566                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2567                 }
2568             }
2569         }
2570     } else { # no-op checkin
2571         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2572             $self->checkin_changed(1);
2573             $self->copy->circ_lib( $self->circ_lib );
2574             $self->update_copy;
2575         }
2576     }
2577
2578     if($self->claims_never_checked_out and 
2579             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2580
2581         # the item was not supposed to be checked out to the user and should now be marked as missing
2582         $self->copy->status(OILS_COPY_STATUS_MISSING);
2583         $self->update_copy;
2584
2585     } else {
2586         $self->reshelve_copy unless $needed_for_something;
2587     }
2588
2589     return if $self->bail_out;
2590
2591     unless($self->checkin_changed) {
2592
2593         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2594         my $stat = $U->copy_status($self->copy->status)->id;
2595
2596         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2597          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2598         $self->bail_out(1); # no need to commit anything
2599
2600     } else {
2601
2602         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2603             unless @{$self->events};
2604     }
2605
2606     $self->finish_fines_and_voiding;
2607
2608     OpenILS::Utils::Penalty->calculate_penalties(
2609         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2610
2611     $self->checkin_flesh_events;
2612     return;
2613 }
2614
2615 sub finish_fines_and_voiding {
2616     my $self = shift;
2617     return unless $self->circ;
2618
2619     return unless $self->backdate or $self->void_overdues;
2620
2621     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2622     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2623
2624     my $evt = $CC->void_or_zero_overdues(
2625         $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
2626
2627     return $self->bail_on_events($evt) if $evt;
2628
2629     # Make sure the circ is open or closed as necessary.
2630     $evt = $U->check_open_xact($self->editor, $self->circ->id);
2631     return $self->bail_on_events($evt) if $evt;
2632
2633     return undef;
2634 }
2635
2636
2637 # if a deposit was payed for this item, push the event
2638 sub check_circ_deposit {
2639     my $self = shift;
2640     return unless $self->circ;
2641     my $deposit = $self->editor->search_money_billing(
2642         {   btype => 5, 
2643             xact => $self->circ->id, 
2644             voided => 'f'
2645         }, {idlist => 1})->[0];
2646
2647     $self->push_events(OpenILS::Event->new(
2648         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2649 }
2650
2651 sub reshelve_copy {
2652    my $self    = shift;
2653    my $force   = $self->force || shift;
2654    my $copy    = $self->copy;
2655
2656    my $stat = $U->copy_status($copy->status)->id;
2657
2658    if($force || (
2659       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2660       $stat != OILS_COPY_STATUS_CATALOGING and
2661       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2662       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2663
2664         $copy->status( OILS_COPY_STATUS_RESHELVING );
2665             $self->update_copy;
2666             $self->checkin_changed(1);
2667     }
2668 }
2669
2670
2671 # Returns true if the item is at the current location
2672 # because it was transited there for a hold and the 
2673 # hold has not been fulfilled
2674 sub checkin_check_holds_shelf {
2675     my $self = shift;
2676     return 0 unless $self->copy;
2677
2678     return 0 unless 
2679         $U->copy_status($self->copy->status)->id ==
2680             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2681
2682     # Attempt to clear shelf expired holds for this copy
2683     $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2684         if($self->clear_expired);
2685
2686     # find the hold that put us on the holds shelf
2687     my $holds = $self->editor->search_action_hold_request(
2688         { 
2689             current_copy => $self->copy->id,
2690             capture_time => { '!=' => undef },
2691             fulfillment_time => undef,
2692             cancel_time => undef,
2693         }
2694     );
2695
2696     unless(@$holds) {
2697         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2698         $self->reshelve_copy(1);
2699         return 0;
2700     }
2701
2702     my $hold = $$holds[0];
2703
2704     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2705         $hold->id. "] for copy ".$self->copy->barcode);
2706
2707     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2708         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2709         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2710             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2711             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2712                 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2713                 $self->fake_hold_dest(1);
2714                 return 1;
2715             }
2716         }
2717     }
2718
2719     if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2720         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2721         return 1;
2722     }
2723
2724     $logger->info("circulator: hold is not for here..");
2725     $self->remote_hold($hold);
2726     return 0;
2727 }
2728
2729
2730 sub checkin_handle_precat {
2731     my $self    = shift;
2732    my $copy    = $self->copy;
2733
2734    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2735         $copy->status(OILS_COPY_STATUS_CATALOGING);
2736         $self->update_copy();
2737         $self->checkin_changed(1);
2738         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2739    }
2740 }
2741
2742
2743 sub checkin_build_copy_transit {
2744     my $self            = shift;
2745     my $dest            = shift;
2746     my $copy       = $self->copy;
2747     my $transit    = Fieldmapper::action::transit_copy->new;
2748
2749     # if we are transiting an item to the shelf shelf, it's a hold transit
2750     if (my $hold = $self->remote_hold) {
2751         $transit = Fieldmapper::action::hold_transit_copy->new;
2752         $transit->hold($hold->id);
2753
2754         # the item is going into transit, remove any shelf-iness
2755         if ($hold->current_shelf_lib or $hold->shelf_time) {
2756             $hold->clear_current_shelf_lib;
2757             $hold->clear_shelf_time;
2758             return $self->bail_on_events($self->editor->event)
2759                 unless $self->editor->update_action_hold_request($hold);
2760         }
2761     }
2762
2763     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2764     $logger->info("circulator: transiting copy to $dest");
2765
2766     $transit->source($self->circ_lib);
2767     $transit->dest($dest);
2768     $transit->target_copy($copy->id);
2769     $transit->source_send_time('now');
2770     $transit->copy_status( $U->copy_status($copy->status)->id );
2771
2772     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2773
2774     if ($self->remote_hold) {
2775         return $self->bail_on_events($self->editor->event)
2776             unless $self->editor->create_action_hold_transit_copy($transit);
2777     } else {
2778         return $self->bail_on_events($self->editor->event)
2779             unless $self->editor->create_action_transit_copy($transit);
2780     }
2781
2782     # ensure the transit is returned to the caller
2783     $self->transit($transit);
2784
2785     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2786     $self->update_copy;
2787     $self->checkin_changed(1);
2788 }
2789
2790
2791 sub hold_capture_is_possible {
2792     my $self = shift;
2793     my $copy = $self->copy;
2794
2795     # we've been explicitly told not to capture any holds
2796     return 0 if $self->capture eq 'nocapture';
2797
2798     # See if this copy can fulfill any holds
2799     my $hold = $holdcode->find_nearest_permitted_hold(
2800         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2801     );
2802     return undef if ref $hold eq "HASH" and
2803         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2804     return $hold;
2805 }
2806
2807 sub reservation_capture_is_possible {
2808     my $self = shift;
2809     my $copy = $self->copy;
2810
2811     # we've been explicitly told not to capture any holds
2812     return 0 if $self->capture eq 'nocapture';
2813
2814     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2815     my $resv = $booking_ses->request(
2816         "open-ils.booking.reservations.could_capture",
2817         $self->editor->authtoken, $copy->barcode
2818     )->gather(1);
2819     $booking_ses->disconnect;
2820     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2821         $self->push_events($resv);
2822     } else {
2823         return $resv;
2824     }
2825 }
2826
2827 # returns true if the item was used (or may potentially be used 
2828 # in subsequent calls) to capture a hold.
2829 sub attempt_checkin_hold_capture {
2830     my $self = shift;
2831     my $copy = $self->copy;
2832
2833     # we've been explicitly told not to capture any holds
2834     return 0 if $self->capture eq 'nocapture';
2835
2836     # See if this copy can fulfill any holds
2837     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2838         $self->editor, $copy, $self->editor->requestor );
2839
2840     if(!$hold) {
2841         $logger->debug("circulator: no potential permitted".
2842             "holds found for copy ".$copy->barcode);
2843         return 0;
2844     }
2845
2846     if($self->capture ne 'capture') {
2847         # see if this item is in a hold-capture-delay location
2848         my $location = $self->copy->location;
2849         if(!ref($location)) {
2850             $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2851             $self->copy->location($location);
2852         }
2853         if($U->is_true($location->hold_verify)) {
2854             $self->bail_on_events(
2855                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2856             return 1;
2857         }
2858     }
2859
2860     $self->retarget($retarget);
2861
2862     my $suppress_transit = 0;
2863     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2864         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2865         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2866             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2867             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2868                 $suppress_transit = 1;
2869                 $hold->pickup_lib($self->circ_lib);
2870             }
2871         }
2872     }
2873
2874     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2875
2876     $hold->current_copy($copy->id);
2877     $hold->capture_time('now');
2878     $self->put_hold_on_shelf($hold) 
2879         if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2880
2881     # prevent DB errors caused by fetching 
2882     # holds from storage, and updating through cstore
2883     $hold->clear_fulfillment_time;
2884     $hold->clear_fulfillment_staff;
2885     $hold->clear_fulfillment_lib;
2886     $hold->clear_expire_time; 
2887     $hold->clear_cancel_time;
2888     $hold->clear_prev_check_time unless $hold->prev_check_time;
2889
2890     $self->bail_on_events($self->editor->event)
2891         unless $self->editor->update_action_hold_request($hold);
2892     $self->hold($hold);
2893     $self->checkin_changed(1);
2894
2895     return 0 if $self->bail_out;
2896
2897     if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2898
2899         if ($hold->hold_type eq 'R') {
2900             $copy->status(OILS_COPY_STATUS_CATALOGING);
2901             $hold->fulfillment_time('now');
2902             $self->noop(1); # Block other transit/hold checks
2903             $self->bail_on_events($self->editor->event)
2904                 unless $self->editor->update_action_hold_request($hold);
2905         } else {
2906             # This hold was captured in the correct location
2907             $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2908             $self->push_events(OpenILS::Event->new('SUCCESS'));
2909
2910             #$self->do_hold_notify($hold->id);
2911             $self->notify_hold($hold->id);
2912         }
2913
2914     } else {
2915     
2916         # Hold needs to be picked up elsewhere.  Build a hold
2917         # transit and route the item.
2918         $self->checkin_build_hold_transit();
2919         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2920         return 0 if $self->bail_out;
2921         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2922     }
2923
2924     # make sure we save the copy status
2925     $self->update_copy;
2926     return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
2927     return 1;
2928 }
2929
2930 sub attempt_checkin_reservation_capture {
2931     my $self = shift;
2932     my $copy = $self->copy;
2933
2934     # we've been explicitly told not to capture any holds
2935     return 0 if $self->capture eq 'nocapture';
2936
2937     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2938     my $evt = $booking_ses->request(
2939         "open-ils.booking.resources.capture_for_reservation",
2940         $self->editor->authtoken,
2941         $copy->barcode,
2942         1 # don't update copy - we probably have it locked
2943     )->gather(1);
2944     $booking_ses->disconnect;
2945
2946     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2947         $logger->warn(
2948             "open-ils.booking.resources.capture_for_reservation " .
2949             "didn't return an event!"
2950         );
2951     } else {
2952         if (
2953             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2954             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2955         ) {
2956             # not-transferable is an error event we'll pass on the user
2957             $logger->warn("reservation capture attempted against non-transferable item");
2958             $self->push_events($evt);
2959             return 0;
2960         } elsif ($evt->{"textcode"} eq "SUCCESS") {
2961             # Re-retrieve copy as reservation capture may have changed
2962             # its status and whatnot.
2963             $logger->info(
2964                 "circulator: booking capture win on copy " . $self->copy->id
2965             );
2966             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2967                 $logger->info(
2968                     "circulator: changing copy " . $self->copy->id .
2969                     "'s status from " . $self->copy->status . " to " .
2970                     $new_copy_status
2971                 );
2972                 $self->copy->status($new_copy_status);
2973                 $self->update_copy;
2974             }
2975             $self->reservation($evt->{"payload"}->{"reservation"});
2976
2977             if (exists $evt->{"payload"}->{"transit"}) {
2978                 $self->push_events(
2979                     new OpenILS::Event(
2980                         "ROUTE_ITEM",
2981                         "org" => $evt->{"payload"}->{"transit"}->dest
2982                     )
2983                 );
2984             }
2985             $self->checkin_changed(1);
2986             return 1;
2987         }
2988     }
2989     # other results are treated as "nothing to capture"
2990     return 0;
2991 }
2992
2993 sub do_hold_notify {
2994     my( $self, $holdid ) = @_;
2995
2996     my $e = new_editor(xact => 1);
2997     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2998     $e->rollback;
2999     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3000     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3001
3002     $logger->info("circulator: running delayed hold notify process");
3003
3004 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3005 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3006
3007     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3008         hold_id => $holdid, requestor => $self->editor->requestor);
3009
3010     $logger->debug("circulator: built hold notifier");
3011
3012     if(!$notifier->event) {
3013
3014         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3015
3016         my $stat = $notifier->send_email_notify;
3017         if( $stat == '1' ) {
3018             $logger->info("circulator: hold notify succeeded for hold $holdid");
3019             return;
3020         } 
3021
3022         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
3023
3024     } else {
3025         $logger->info("circulator: Not sending hold notification since the patron has no email address");
3026     }
3027 }
3028
3029 sub retarget_holds {
3030     my $self = shift;
3031     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3032     my $ses = OpenSRF::AppSession->create('open-ils.storage');
3033     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3034     # no reason to wait for the return value
3035     return;
3036 }
3037
3038 sub checkin_build_hold_transit {
3039     my $self = shift;
3040
3041    my $copy = $self->copy;
3042    my $hold = $self->hold;
3043    my $trans = Fieldmapper::action::hold_transit_copy->new;
3044
3045     $logger->debug("circulator: building hold transit for ".$copy->barcode);
3046
3047    $trans->hold($hold->id);
3048    $trans->source($self->circ_lib);
3049    $trans->dest($hold->pickup_lib);
3050    $trans->source_send_time("now");
3051    $trans->target_copy($copy->id);
3052
3053     # when the copy gets to its destination, it will recover
3054     # this status - put it onto the holds shelf
3055    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3056
3057     return $self->bail_on_events($self->editor->event)
3058         unless $self->editor->create_action_hold_transit_copy($trans);
3059 }
3060
3061
3062
3063 sub process_received_transit {
3064     my $self = shift;
3065     my $copy = $self->copy;
3066     my $copyid = $self->copy->id;
3067
3068     my $status_name = $U->copy_status($copy->status)->name;
3069     $logger->debug("circulator: attempting transit receive on ".
3070         "copy $copyid. Copy status is $status_name");
3071
3072     my $transit = $self->transit;
3073
3074     # Check if we are in a transit suppress range
3075     my $suppress_transit = 0;
3076     if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3077         my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ?  'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3078         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3079         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3080             my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3081             if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3082                 $suppress_transit = 1;
3083                 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3084             }
3085         }
3086     }
3087     if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3088         # - this item is in-transit to a different location
3089         # - Or we are capturing holds as transits, so why create a new transit?
3090
3091         my $tid = $transit->id; 
3092         my $loc = $self->circ_lib;
3093         my $dest = $transit->dest;
3094
3095         $logger->info("circulator: Fowarding transit on copy which is destined ".
3096             "for a different location. transit=$tid, copy=$copyid, current ".
3097             "location=$loc, destination location=$dest");
3098
3099         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3100
3101         # grab the associated hold object if available
3102         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3103         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3104
3105         return $self->bail_on_events($evt);
3106     }
3107
3108     # The transit is received, set the receive time
3109     $transit->dest_recv_time('now');
3110     $self->bail_on_events($self->editor->event)
3111         unless $self->editor->update_action_transit_copy($transit);
3112
3113     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3114
3115     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3116     $copy->status( $transit->copy_status );
3117     $self->update_copy();
3118     return if $self->bail_out;
3119
3120     my $ishold = 0;
3121     if($hold_transit) { 
3122         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3123
3124         if ($hold) {
3125             # hold has arrived at destination, set shelf time
3126             $self->put_hold_on_shelf($hold);
3127             $self->bail_on_events($self->editor->event)
3128                 unless $self->editor->update_action_hold_request($hold);
3129             return if $self->bail_out;
3130
3131             $self->notify_hold($hold_transit->hold);
3132             $ishold = 1;
3133         } else {
3134             $hold_transit = undef;
3135             $self->cancelled_hold_transit(1);
3136             $self->reshelve_copy(1);
3137             $self->fake_hold_dest(0);
3138         }
3139     }
3140
3141     $self->push_events( 
3142         OpenILS::Event->new(
3143         'SUCCESS', 
3144         ishold => $ishold,
3145       payload => { transit => $transit, holdtransit => $hold_transit } ));
3146
3147     return $hold_transit;
3148 }
3149
3150
3151 # ------------------------------------------------------------------
3152 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3153 # ------------------------------------------------------------------
3154 sub put_hold_on_shelf {
3155     my($self, $hold) = @_;
3156     $hold->shelf_time('now');
3157     $hold->current_shelf_lib($self->circ_lib);
3158     $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3159     return undef;
3160 }
3161
3162 sub handle_fines {
3163    my $self = shift;
3164    my $reservation = shift;
3165    my $dt_parser = DateTime::Format::ISO8601->new;
3166
3167    my $obj = $reservation ? $self->reservation : $self->circ;
3168
3169     my $lost_bill_opts = $self->lost_bill_options;
3170     my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3171     # first, restore any voided overdues for lost, if needed
3172     if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3173         my $restore_od = $U->ou_ancestor_setting_value(
3174             $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3175             $self->editor) || 0;
3176         $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3177             if $restore_od;
3178     }
3179
3180     # next, handle normal overdue generation and apply stop_fines
3181     # XXX reservations don't have stop_fines
3182     # TODO revisit booking_reservation re: stop_fines support
3183     if ($reservation or !$obj->stop_fines) {
3184         my $skip_for_grace;
3185
3186         # This is a crude check for whether we are in a grace period. The code
3187         # in generate_fines() does a more thorough job, so this exists solely
3188         # as a small optimization, and might be better off removed.
3189
3190         # If we have a grace period
3191         if($obj->can('grace_period')) {
3192             # Parse out the due date
3193             my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3194             # Add the grace period to the due date
3195             $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3196             # Don't generate fines on circs still in grace period
3197             $skip_for_grace = $due_date > DateTime->now;
3198         }
3199         $CC->generate_fines({circs => [$obj], editor => $self->editor})
3200             unless $skip_for_grace;
3201
3202         if (!$reservation and !$obj->stop_fines) {
3203             $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3204             $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3205             $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3206             $obj->stop_fines_time('now');
3207             $obj->stop_fines_time($self->backdate) if $self->backdate;
3208             $self->editor->update_action_circulation($obj);
3209         }
3210     }
3211
3212     # finally, handle voiding of lost item and processing fees
3213     if ($self->needs_lost_bill_handling) {
3214         my $void_cost = $U->ou_ancestor_setting_value(
3215             $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3216             $self->editor) || 0;
3217         my $void_proc_fee = $U->ou_ancestor_setting_value(
3218             $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3219             $self->editor) || 0;
3220         $self->checkin_handle_lost_or_lo_now_found(
3221             $lost_bill_opts->{void_cost_btype},
3222             $lost_bill_opts->{is_longoverdue}) if $void_cost;
3223         $self->checkin_handle_lost_or_lo_now_found(
3224             $lost_bill_opts->{void_fee_btype},
3225             $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3226     }
3227
3228    return undef;
3229 }
3230
3231 sub checkin_handle_circ_start {
3232    my $self = shift;
3233    my $circ = $self->circ;
3234    my $copy = $self->copy;
3235    my $evt;
3236    my $obt;
3237
3238    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3239
3240    # backdate the circ if necessary
3241    if($self->backdate) {
3242         my $evt = $self->checkin_handle_backdate;
3243         return $self->bail_on_events($evt) if $evt;
3244    }
3245
3246     # Set the checkin vars since we have the item
3247     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3248
3249     # capture the true scan time for back-dated checkins
3250     $circ->checkin_scan_time('now');
3251
3252     $circ->checkin_staff($self->editor->requestor->id);
3253     $circ->checkin_lib($self->circ_lib);
3254     $circ->checkin_workstation($self->editor->requestor->wsid);
3255
3256     my $circ_lib = (ref $self->copy->circ_lib) ?  
3257         $self->copy->circ_lib->id : $self->copy->circ_lib;
3258     my $stat = $U->copy_status($self->copy->status)->id;
3259
3260     if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3261         # we will now handle lost fines, but the copy will retain its 'lost'
3262         # status if it needs to transit home unless lost_immediately_available
3263         # is true
3264         #
3265         # if we decide to also delay fine handling until the item arrives home,
3266         # we will need to call lost fine handling code both when checking items
3267         # in and also when receiving transits
3268         $self->checkin_handle_lost($circ_lib);
3269     } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3270         # same process as above.
3271         $self->checkin_handle_long_overdue($circ_lib);
3272     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3273         $logger->info("circulator: not updating copy status on checkin because copy is missing");
3274     } else {
3275         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3276         $self->update_copy;
3277     }
3278
3279     return undef;
3280 }
3281
3282 sub checkin_handle_circ_finish {
3283     my $self = shift;
3284     my $circ = $self->circ;
3285
3286     # see if there are any fines owed on this circ.  if not, close it
3287     my ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3288     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3289
3290     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3291
3292     return $self->bail_on_events($self->editor->event)
3293         unless $self->editor->update_action_circulation($circ);
3294
3295     return undef;
3296 }
3297
3298 # ------------------------------------------------------------------
3299 # See if we need to void billings, etc. for lost checkin
3300 # ------------------------------------------------------------------
3301 sub checkin_handle_lost {
3302     my $self = shift;
3303     my $circ_lib = shift;
3304
3305     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3306         OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3307
3308     $self->lost_bill_options({
3309         circ_lib => $circ_lib,
3310         ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3311         ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3312         ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3313         void_cost_btype => 3, 
3314         void_fee_btype => 4 
3315     });
3316
3317     return $self->checkin_handle_lost_or_longoverdue(
3318         circ_lib => $circ_lib,
3319         max_return => $max_return,
3320         ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3321         ous_use_last_activity => undef # not supported for LOST checkin
3322     );
3323 }
3324
3325 # ------------------------------------------------------------------
3326 # See if we need to void billings, etc. for long-overdue checkin
3327 # note: not using constants below since they serve little purpose 
3328 # for single-use strings that are descriptive in their own right 
3329 # and mostly just complicate debugging.
3330 # ------------------------------------------------------------------
3331 sub checkin_handle_long_overdue {
3332     my $self = shift;
3333     my $circ_lib = shift;
3334
3335     $logger->info("circulator: processing long-overdue checkin...");
3336
3337     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3338         'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3339
3340     $self->lost_bill_options({
3341         circ_lib => $circ_lib,
3342         ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3343         ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3344         is_longoverdue => 1,
3345         ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3346         void_cost_btype => 10,
3347         void_fee_btype => 11
3348     });
3349
3350     return $self->checkin_handle_lost_or_longoverdue(
3351         circ_lib => $circ_lib,
3352         max_return => $max_return,
3353         ous_immediately_available => 'circ.longoverdue_immediately_available',
3354         ous_use_last_activity => 
3355             'circ.longoverdue.use_last_activity_date_on_return'
3356     )
3357 }
3358
3359 # last billing activity is last payment time, last billing time, or the 
3360 # circ due date.  If the relevant "use last activity" org unit setting is 
3361 # false/unset, then last billing activity is always the due date.
3362 sub get_circ_last_billing_activity {
3363     my $self = shift;
3364     my $circ_lib = shift;
3365     my $setting = shift;
3366     my $date = $self->circ->due_date;
3367
3368     return $date unless $setting and 
3369         $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3370
3371     my $xact = $self->editor->retrieve_money_billable_transaction([
3372         $self->circ->id,
3373         {flesh => 1, flesh_fields => {mbt => ['summary']}}
3374     ]);
3375
3376     if ($xact->summary) {
3377         $date = $xact->summary->last_payment_ts || 
3378                 $xact->summary->last_billing_ts || 
3379                 $self->circ->due_date;
3380     }
3381
3382     return $date;
3383 }
3384
3385
3386 sub checkin_handle_lost_or_longoverdue {
3387     my ($self, %args) = @_;
3388
3389     my $circ = $self->circ;
3390     my $max_return = $args{max_return};
3391     my $circ_lib = $args{circ_lib};
3392
3393     if ($max_return) {
3394
3395         my $last_activity = 
3396             $self->get_circ_last_billing_activity(
3397                 $circ_lib, $args{ous_use_last_activity});
3398
3399         my $today = time();
3400         my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3401         $tm[5] -= 1 if $tm[5] > 0;
3402         my $due = timelocal(int($tm[1]), int($tm[2]), 
3403             int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3404
3405         my $last_chance = 
3406             OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3407
3408         $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3409             "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3410                 "DUE: $due LAST: $last_chance");
3411
3412         $max_return = 0 if $today < $last_chance;
3413     }
3414
3415
3416     if ($max_return) {
3417
3418         $logger->info("circulator: check-in of lost/lo item exceeds max ". 
3419             "return interval.  skipping fine/fee voiding, etc.");
3420
3421     } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3422
3423         $logger->info("circulator: check-in of lost/lo item having a balance ".
3424             "of zero, skipping fine/fee voiding and reinstatement.");
3425
3426     } else { # within max-return interval or no interval defined
3427
3428         $logger->info("circulator: check-in of lost/lo item is within the ".
3429             "max return interval (or no interval is defined).  Proceeding ".
3430             "with fine/fee voiding, etc.");
3431
3432         $self->needs_lost_bill_handling(1);
3433     }
3434
3435     if ($circ_lib != $self->circ_lib) {
3436         # if the item is not home, check to see if we want to retain the
3437         # lost/longoverdue status at this point in the process
3438
3439         my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, 
3440             $args{ous_immediately_available}, $self->editor) || 0;
3441
3442         if ($immediately_available) {
3443             # item status does not need to be retained, so give it a
3444             # reshelving status as if it were a normal checkin
3445             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3446             $self->update_copy;
3447         } else {
3448             $logger->info("circulator: leaving lost/longoverdue copy".
3449                 " status in place on checkin");
3450         }
3451     } else {
3452         # lost/longoverdue item is home and processed, treat like a normal 
3453         # checkin from this point on
3454         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3455         $self->update_copy;
3456     }
3457 }
3458
3459
3460 sub checkin_handle_backdate {
3461     my $self = shift;
3462
3463     # ------------------------------------------------------------------
3464     # clean up the backdate for date comparison
3465     # XXX We are currently taking the due-time from the original due-date,
3466     # not the input.  Do we need to do this?  This certainly interferes with
3467     # backdating of hourly checkouts, but that is likely a very rare case.
3468     # ------------------------------------------------------------------
3469     my $bd = cleanse_ISO8601($self->backdate);
3470     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3471     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3472     $new_date->set_hour($original_date->hour());
3473     $new_date->set_minute($original_date->minute());
3474     if ($new_date >= DateTime->now) {
3475         # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3476         # $self->backdate() autoload handler ignores undef values.  
3477         # Clear the backdate manually.
3478         $logger->info("circulator: ignoring future backdate: $new_date");
3479         delete $self->{backdate};
3480     } else {
3481         $self->backdate(cleanse_ISO8601($new_date->datetime()));
3482     }
3483
3484     return undef;
3485 }
3486
3487
3488 sub check_checkin_copy_status {
3489     my $self = shift;
3490    my $copy = $self->copy;
3491
3492    my $status = $U->copy_status($copy->status)->id;
3493
3494    return undef
3495       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3496             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3497             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3498             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3499             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3500             $status == OILS_COPY_STATUS_CATALOGING  ||
3501             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3502             $status == OILS_COPY_STATUS_RESHELVING );
3503
3504    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3505       if( $status == OILS_COPY_STATUS_LOST );
3506
3507     return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3508         if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3509
3510    return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3511       if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3512
3513    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3514       if( $status == OILS_COPY_STATUS_MISSING );
3515
3516    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3517 }
3518
3519
3520
3521 # --------------------------------------------------------------------------
3522 # On checkin, we need to return as many relevant objects as we can
3523 # --------------------------------------------------------------------------
3524 sub checkin_flesh_events {
3525     my $self = shift;
3526
3527     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3528         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3529             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3530     }
3531
3532     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3533
3534     my $hold;
3535     if($self->hold and !$self->hold->cancel_time) {
3536         $hold = $self->hold;
3537         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3538     }
3539
3540     if($self->circ) {
3541         # update our copy of the circ object and 
3542         # flesh the billing summary data
3543         $self->circ(
3544             $self->editor->retrieve_action_circulation([
3545                 $self->circ->id, {
3546                     flesh => 2,
3547                     flesh_fields => {
3548                         circ => ['billable_transaction'],
3549                         mbt => ['summary']
3550                     }
3551                 }
3552             ])
3553         );
3554     }
3555
3556     if($self->patron) {
3557         # flesh some patron fields before returning
3558         $self->patron(
3559             $self->editor->retrieve_actor_user([
3560                 $self->patron->id,
3561                 {
3562                     flesh => 1,
3563                     flesh_fields => {
3564                         au => ['card', 'billing_address', 'mailing_address']
3565                     }
3566                 }
3567             ])
3568         );
3569     }
3570
3571     for my $evt (@{$self->events}) {
3572
3573         my $payload         = {};
3574         $payload->{copy}    = $U->unflesh_copy($self->copy);
3575         $payload->{volume}  = $self->volume;
3576         $payload->{record}  = $record,
3577         $payload->{circ}    = $self->circ;
3578         $payload->{transit} = $self->transit;
3579         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3580         $payload->{hold}    = $hold;
3581         $payload->{patron}  = $self->patron;
3582         $payload->{reservation} = $self->reservation
3583             unless (not $self->reservation or $self->reservation->cancel_time);
3584
3585         $evt->{payload}     = $payload;
3586     }
3587 }
3588
3589 sub log_me {
3590     my( $self, $msg ) = @_;
3591     my $bc = ($self->copy) ? $self->copy->barcode :
3592         $self->barcode;
3593     $bc ||= "";
3594     my $usr = ($self->patron) ? $self->patron->id : "";
3595     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3596         ", recipient=$usr, copy=$bc");
3597 }
3598
3599
3600 sub do_renew {
3601     my $self = shift;
3602     $self->log_me("do_renew()");
3603
3604     # Make sure there is an open circ to renew
3605     my $usrid = $self->patron->id if $self->patron;
3606     my $circ = $self->editor->search_action_circulation({
3607         target_copy => $self->copy->id,
3608         xact_finish => undef,
3609         checkin_time => undef,
3610         ($usrid ? (usr => $usrid) : ())
3611     })->[0];
3612
3613     return $self->bail_on_events($self->editor->event) unless $circ;
3614
3615     # A user is not allowed to renew another user's items without permission
3616     unless( $circ->usr eq $self->editor->requestor->id ) {
3617         return $self->bail_on_events($self->editor->events)
3618             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3619     }   
3620
3621     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3622         if $circ->renewal_remaining < 1;
3623
3624     # -----------------------------------------------------------------
3625
3626     $self->parent_circ($circ->id);
3627     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3628     $self->circ($circ);
3629
3630     # Opac renewal - re-use circ library from original circ (unless told not to)
3631     if($self->opac_renewal) {
3632         unless(defined($opac_renewal_use_circ_lib)) {
3633             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3634             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3635                 $opac_renewal_use_circ_lib = 1;
3636             }
3637             else {
3638                 $opac_renewal_use_circ_lib = 0;
3639             }
3640         }
3641         $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3642     }
3643
3644     # Desk renewal - re-use circ library from original circ (unless told not to)
3645     if($self->desk_renewal) {
3646         unless(defined($desk_renewal_use_circ_lib)) {
3647             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3648             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3649                 $desk_renewal_use_circ_lib = 1;
3650             }
3651             else {
3652                 $desk_renewal_use_circ_lib = 0;
3653             }
3654         }
3655         $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3656     }
3657
3658     # Run the fine generator against the old circ
3659     # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3660     # a few lines down.  Commenting out, for now.
3661     #$self->handle_fines;
3662
3663     $self->run_renew_permit;
3664
3665     # Check the item in
3666     $self->do_checkin();
3667     return if $self->bail_out;
3668
3669     unless( $self->permit_override ) {
3670         $self->do_permit();
3671         return if $self->bail_out;
3672         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3673         $self->remove_event('ITEM_NOT_CATALOGED');
3674     }   
3675
3676     $self->override_events;
3677     return if $self->bail_out;
3678
3679     $self->events([]);
3680     $self->do_checkout();
3681 }
3682
3683
3684 sub remove_event {
3685     my( $self, $evt ) = @_;
3686     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3687     $logger->debug("circulator: removing event from list: $evt");
3688     my @events = @{$self->events};
3689     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3690 }
3691
3692
3693 sub have_event {
3694     my( $self, $evt ) = @_;
3695     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3696     return grep { $_->{textcode} eq $evt } @{$self->events};
3697 }
3698
3699
3700 sub run_renew_permit {
3701     my $self = shift;
3702
3703     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3704         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3705             $self->editor, $self->copy, $self->editor->requestor, 1
3706         );
3707         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3708     }
3709
3710     my $results = $self->run_indb_circ_test;
3711     $self->push_events($self->matrix_test_result_events)
3712         unless $self->circ_test_success;
3713 }
3714
3715
3716 # XXX: The primary mechanism for storing circ history is now handled
3717 # by tracking real circulation objects instead of bibs in a bucket.
3718 # However, this code is disabled by default and could be useful 
3719 # some day, so may as well leave it for now.
3720 sub append_reading_list {
3721     my $self = shift;
3722
3723     return undef unless 
3724         $self->is_checkout and 
3725         $self->patron and 
3726         $self->copy and 
3727         !$self->is_noncat;
3728
3729
3730     # verify history is globally enabled and uses the bucket mechanism
3731     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3732         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3733
3734     return undef unless $htype and $htype eq 'bucket';
3735
3736     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3737
3738     # verify the patron wants to retain the hisory
3739     my $setting = $e->search_actor_user_setting(
3740         {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3741     
3742     unless($setting and $setting->value) {
3743         $e->rollback;
3744         return undef;
3745     }
3746
3747     my $bkt = $e->search_container_copy_bucket(
3748         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3749
3750     my $pos = 1;
3751
3752     if($bkt) {
3753         # find the next item position
3754         my $last_item = $e->search_container_copy_bucket_item(
3755             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3756         $pos = $last_item->pos + 1 if $last_item;
3757
3758     } else {
3759         # create the history bucket if necessary
3760         $bkt = Fieldmapper::container::copy_bucket->new;
3761         $bkt->owner($self->patron->id);
3762         $bkt->name('');
3763         $bkt->btype('circ_history');
3764         $bkt->pub('f');
3765         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3766     }
3767
3768     my $item = Fieldmapper::container::copy_bucket_item->new;
3769
3770     $item->bucket($bkt->id);
3771     $item->target_copy($self->copy->id);
3772     $item->pos($pos);
3773
3774     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3775     $e->commit;
3776
3777     return undef;
3778 }
3779
3780
3781 sub make_trigger_events {
3782     my $self = shift;
3783     return unless $self->circ;
3784     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3785     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3786     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3787 }
3788
3789
3790
3791 sub checkin_handle_lost_or_lo_now_found {
3792     my ($self, $bill_type, $is_longoverdue) = @_;
3793
3794     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3795
3796     $logger->debug("voiding $tag item billings");
3797     my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
3798     $self->bail_on_events($self->editor->event) if ($result);
3799 }
3800
3801 sub checkin_handle_lost_or_lo_now_found_restore_od {
3802     my $self = shift;
3803     my $circ_lib = shift;
3804     my $is_longoverdue = shift;
3805     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3806
3807     # ------------------------------------------------------------------
3808     # restore those overdue charges voided when item was set to lost
3809     # ------------------------------------------------------------------
3810
3811     my $ods = $self->editor->search_money_billing([
3812         {
3813             xact => $self->circ->id,
3814             btype => 1
3815         },
3816         {
3817             order_by => {mb => 'billing_ts desc'}
3818         }
3819     ]);
3820
3821     $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
3822     # Because actual users get up to all kinds of unexpectedness, we
3823     # only recreate up to $circ->max_fine in bills.  I know you think
3824     # it wouldn't happen that bills could get created, voided, and
3825     # recreated more than once, but I guaran-damn-tee you that it will
3826     # happen.
3827     if ($ods && @$ods) {
3828         my $void_amount = 0;
3829         my $void_max = $self->circ->max_fine();
3830         # search for overdues voided the new way (aka "adjusted")
3831         my @billings = map {$_->id()} @$ods;
3832         my $voids = $self->editor->search_money_account_adjustment(
3833             {
3834                 billing => \@billings
3835             }
3836         );
3837         if (@$voids) {
3838             map {$void_amount += $_->amount()} @$voids;
3839         } else {
3840             # if no adjustments found, assume they were voided the old way (aka "voided")
3841             for my $bill (@$ods) {
3842                 if( $U->is_true($bill->voided) ) {
3843                     $void_amount += $bill->amount();
3844                 }
3845             }
3846         }
3847         $CC->create_bill(
3848             $self->editor,
3849             ($void_amount < $void_max ? $void_amount : $void_max),
3850             $ods->[0]->btype(),
3851             $ods->[0]->billing_type(),
3852             $self->circ->id(),
3853             "System: $tag RETURNED - OVERDUES REINSTATED",
3854             $ods->[0]->billing_ts() # date this restoration the same as the last overdue (for possible subsequent fine generation)
3855         );
3856     }
3857 }
3858
3859 1;