]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
LP 1499123: Modify Perl code for csp.ignore_proximity field.
[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     # With the addition of ignore_proximity in csp, we need to fetch
1486     # the proximity of both the circ_lib and the copy's circ_lib to
1487     # the patron's home_ou.
1488     my ($ou_prox, $copy_prox);
1489     my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
1490     $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
1491     $ou_prox = -1 unless (defined($ou_prox));
1492     my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
1493     if ($copy_ou == $self->circ_lib) {
1494         # Save us the time of an extra query.
1495         $copy_prox = $ou_prox;
1496     } else {
1497         $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
1498         $copy_prox = -1 unless (defined($copy_prox));
1499     }
1500
1501     # See if the user has any penalties applied that prevent hold fulfillment
1502     my $pens = $self->editor->json_query({
1503         select => {csp => ['name', 'label']},
1504         from => {ausp => {csp => {}}},
1505         where => {
1506             '+ausp' => {
1507                 usr => $self->patron->id,
1508                 org_unit => $U->get_org_full_path($self->circ_lib),
1509                 '-or' => [
1510                     {stop_date => undef},
1511                     {stop_date => {'>' => 'now'}}
1512                 ]
1513             },
1514             '+csp' => {
1515                 block_list => {'like' => '%FULFILL%'},
1516                 '-or' => [
1517                     {ignore_proximity => undef},
1518                     {ignore_proximity => {'<' => $ou_prox}},
1519                     {ignore_proximity => {'<' => $copy_prox}}
1520                 ]
1521             }
1522         }
1523     });
1524
1525     return 0 unless @$pens;
1526
1527     for my $pen (@$pens) {
1528         $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1529         my $event = OpenILS::Event->new($pen->{name});
1530         $event->{desc} = $pen->{label};
1531         $self->push_events($event);
1532     }
1533
1534     $self->override_events;
1535     return $self->bail_out;
1536 }
1537
1538
1539 # ------------------------------------------------------------------------------
1540 # When an item is checked out, see if we can fulfill a hold for this patron
1541 # ------------------------------------------------------------------------------
1542 sub handle_checkout_holds {
1543    my $self    = shift;
1544    my $copy    = $self->copy;
1545    my $patron  = $self->patron;
1546
1547    my $e = $self->editor;
1548    $self->fulfilled_holds([]);
1549
1550    # non-cats can't fulfill a hold
1551    return if $self->is_noncat;
1552
1553     my $hold = $e->search_action_hold_request({   
1554         current_copy        => $copy->id , 
1555         cancel_time         => undef, 
1556         fulfillment_time    => undef,
1557         '-or' => [
1558             {expire_time => undef},
1559             {expire_time => {'>' => 'now'}}
1560         ]
1561     })->[0];
1562
1563     if($hold and $hold->usr != $patron->id) {
1564         # reset the hold since the copy is now checked out
1565     
1566         $logger->info("circulator: un-targeting hold ".$hold->id.
1567             " because copy ".$copy->id." is getting checked out");
1568
1569         $hold->clear_prev_check_time; 
1570         $hold->clear_current_copy;
1571         $hold->clear_capture_time;
1572         $hold->clear_shelf_time;
1573         $hold->clear_shelf_expire_time;
1574         $hold->clear_current_shelf_lib;
1575
1576         return $self->bail_on_event($e->event)
1577             unless $e->update_action_hold_request($hold);
1578
1579         $hold = undef;
1580     }
1581
1582     unless($hold) {
1583         $hold = $self->find_related_user_hold($copy, $patron) or return;
1584         $logger->info("circulator: found related hold to fulfill in checkout");
1585     }
1586
1587     return if $self->check_hold_fulfill_blocks;
1588
1589     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1590
1591     # if the hold was never officially captured, capture it.
1592     $hold->current_copy($copy->id);
1593     $hold->capture_time('now') unless $hold->capture_time;
1594     $hold->fulfillment_time('now');
1595     $hold->fulfillment_staff($e->requestor->id);
1596     $hold->fulfillment_lib($self->circ_lib);
1597
1598     return $self->bail_on_events($e->event)
1599         unless $e->update_action_hold_request($hold);
1600
1601     return $self->fulfilled_holds([$hold->id]);
1602 }
1603
1604
1605 # ------------------------------------------------------------------------------
1606 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1607 # the patron directly targets the checked out item, see if there is another hold 
1608 # for the patron that could be fulfilled by the checked out item.  Fulfill the
1609 # oldest hold and only fulfill 1 of them.
1610
1611 # For "another hold":
1612 #
1613 # First, check for one that the copy matches via hold_copy_map, ensuring that
1614 # *any* hold type that this copy could fill may end up filled.
1615 #
1616 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1617 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1618 # that are non-requestable to count as capturing those hold types.
1619 # ------------------------------------------------------------------------------
1620 sub find_related_user_hold {
1621     my($self, $copy, $patron) = @_;
1622     my $e = $self->editor;
1623
1624     # holds on precat copies are always copy-level, so this call will
1625     # always return undef.  Exit early.
1626     return undef if $self->is_precat;
1627
1628     return undef unless $U->ou_ancestor_setting_value(        
1629         $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1630
1631     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1632     my $args = {
1633         select => {ahr => ['id']}, 
1634         from => {
1635             ahr => {
1636                 ahcm => {
1637                     field => 'hold',
1638                     fkey => 'id'
1639                 },
1640                 acp => {
1641                     field => 'id', 
1642                     fkey => 'current_copy',
1643                     type => 'left' # there may be no current_copy
1644                 }
1645             }
1646         }, 
1647         where => {
1648             '+ahr' => {
1649                 usr => $patron->id,
1650                 fulfillment_time => undef,
1651                 cancel_time => undef,
1652                '-or' => [
1653                     {expire_time => undef},
1654                     {expire_time => {'>' => 'now'}}
1655                 ]
1656             },
1657             '+ahcm' => {
1658                 target_copy => $self->copy->id
1659             },
1660             '+acp' => {
1661                 '-or' => [
1662                     {id => undef}, # left-join copy may be nonexistent
1663                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1664                 ]
1665             }
1666         },
1667         order_by => {ahr => {request_time => {direction => 'asc'}}},
1668         limit => 1
1669     };
1670
1671     my $hold_info = $e->json_query($args)->[0];
1672     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1673     return undef if $U->ou_ancestor_setting_value(        
1674         $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1675
1676     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1677     $args = {
1678         select => {ahr => ['id']}, 
1679         from => {
1680             ahr => {
1681                 acp => {
1682                     field => 'id', 
1683                     fkey => 'current_copy',
1684                     type => 'left' # there may be no current_copy
1685                 }
1686             }
1687         }, 
1688         where => {
1689             '+ahr' => {
1690                 usr => $patron->id,
1691                 fulfillment_time => undef,
1692                 cancel_time => undef,
1693                '-or' => [
1694                     {expire_time => undef},
1695                     {expire_time => {'>' => 'now'}}
1696                 ]
1697             },
1698             '-or' => [
1699                 {
1700                     '+ahr' => { 
1701                         hold_type => 'V',
1702                         target => $self->volume->id
1703                     }
1704                 },
1705                 { 
1706                     '+ahr' => { 
1707                         hold_type => 'T',
1708                         target => $self->title->id
1709                     }
1710                 },
1711             ],
1712             '+acp' => {
1713                 '-or' => [
1714                     {id => undef}, # left-join copy may be nonexistent
1715                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1716                 ]
1717             }
1718         },
1719         order_by => {ahr => {request_time => {direction => 'asc'}}},
1720         limit => 1
1721     };
1722
1723     $hold_info = $e->json_query($args)->[0];
1724     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1725     return undef;
1726 }
1727
1728
1729 sub run_checkout_scripts {
1730     my $self = shift;
1731     my $nobail = shift;
1732
1733     my $evt;
1734
1735     my $duration;
1736     my $recurring;
1737     my $max_fine;
1738     my $hard_due_date;
1739     my $duration_name;
1740     my $recurring_name;
1741     my $max_fine_name;
1742     my $hard_due_date_name;
1743
1744     $self->run_indb_circ_test();
1745     $duration = $self->circ_matrix_matchpoint->duration_rule;
1746     $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1747     $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1748     $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1749
1750     $duration_name = $duration->name if $duration;
1751     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1752
1753         unless($duration) {
1754             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1755             return $self->bail_on_events($evt) if ($evt && !$nobail);
1756         
1757             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1758             return $self->bail_on_events($evt) if ($evt && !$nobail);
1759         
1760             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1761             return $self->bail_on_events($evt) if ($evt && !$nobail);
1762
1763             if($hard_due_date_name) {
1764                 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1765                 return $self->bail_on_events($evt) if ($evt && !$nobail);
1766             }
1767         }
1768
1769     } else {
1770
1771         # The item circulates with an unlimited duration
1772         $duration   = undef;
1773         $recurring  = undef;
1774         $max_fine   = undef;
1775         $hard_due_date = undef;
1776     }
1777
1778    $self->duration_rule($duration);
1779    $self->recurring_fines_rule($recurring);
1780    $self->max_fine_rule($max_fine);
1781    $self->hard_due_date($hard_due_date);
1782 }
1783
1784
1785 sub build_checkout_circ_object {
1786     my $self = shift;
1787
1788    my $circ       = Fieldmapper::action::circulation->new;
1789    my $duration   = $self->duration_rule;
1790    my $max        = $self->max_fine_rule;
1791    my $recurring  = $self->recurring_fines_rule;
1792    my $hard_due_date    = $self->hard_due_date;
1793    my $copy       = $self->copy;
1794    my $patron     = $self->patron;
1795    my $duration_date_ceiling;
1796    my $duration_date_ceiling_force;
1797
1798     if( $duration ) {
1799
1800         my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1801         $duration_date_ceiling = $policy->{duration_date_ceiling};
1802         $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1803
1804         my $dname = $duration->name;
1805         my $mname = $max->name;
1806         my $rname = $recurring->name;
1807         my $hdname = ''; 
1808         if($hard_due_date) {
1809             $hdname = $hard_due_date->name;
1810         }
1811
1812         $logger->debug("circulator: building circulation ".
1813             "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1814     
1815         $circ->duration($policy->{duration});
1816         $circ->recurring_fine($policy->{recurring_fine});
1817         $circ->duration_rule($duration->name);
1818         $circ->recurring_fine_rule($recurring->name);
1819         $circ->max_fine_rule($max->name);
1820         $circ->max_fine($policy->{max_fine});
1821         $circ->fine_interval($recurring->recurrence_interval);
1822         $circ->renewal_remaining($duration->max_renewals);
1823         $circ->grace_period($policy->{grace_period});
1824
1825     } else {
1826
1827         $logger->info("circulator: copy found with an unlimited circ duration");
1828         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1829         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1830         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1831         $circ->renewal_remaining(0);
1832         $circ->grace_period(0);
1833     }
1834
1835    $circ->target_copy( $copy->id );
1836    $circ->usr( $patron->id );
1837    $circ->circ_lib( $self->circ_lib );
1838    $circ->workstation($self->editor->requestor->wsid) 
1839     if defined $self->editor->requestor->wsid;
1840
1841     # renewals maintain a link to the parent circulation
1842     $circ->parent_circ($self->parent_circ);
1843
1844    if( $self->is_renewal ) {
1845       $circ->opac_renewal('t') if $self->opac_renewal;
1846       $circ->phone_renewal('t') if $self->phone_renewal;
1847       $circ->desk_renewal('t') if $self->desk_renewal;
1848       $circ->renewal_remaining($self->renewal_remaining);
1849       $circ->circ_staff($self->editor->requestor->id);
1850    }
1851
1852
1853     # if the user provided an overiding checkout time,
1854     # (e.g. the checkout really happened several hours ago), then
1855     # we apply that here.  Does this need a perm??
1856     $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1857         if $self->checkout_time;
1858
1859     # if a patron is renewing, 'requestor' will be the patron
1860     $circ->circ_staff($self->editor->requestor->id);
1861     $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
1862
1863     $self->circ($circ);
1864 }
1865
1866 sub do_reservation_pickup {
1867     my $self = shift;
1868
1869     $self->log_me("do_reservation_pickup()");
1870
1871     $self->reservation->pickup_time('now');
1872
1873     if (
1874         $self->reservation->current_resource &&
1875         $U->is_true($self->reservation->target_resource_type->catalog_item)
1876     ) {
1877         # We used to try to set $self->copy and $self->patron here,
1878         # but that should already be done.
1879
1880         $self->run_checkout_scripts(1);
1881
1882         my $duration   = $self->duration_rule;
1883         my $max        = $self->max_fine_rule;
1884         my $recurring  = $self->recurring_fines_rule;
1885
1886         if ($duration && $max && $recurring) {
1887             my $policy = $self->get_circ_policy($duration, $recurring, $max);
1888
1889             my $dname = $duration->name;
1890             my $mname = $max->name;
1891             my $rname = $recurring->name;
1892
1893             $logger->debug("circulator: updating reservation ".
1894                 "with duration=$dname, maxfine=$mname, recurring=$rname");
1895
1896             $self->reservation->fine_amount($policy->{recurring_fine});
1897             $self->reservation->max_fine($policy->{max_fine});
1898             $self->reservation->fine_interval($recurring->recurrence_interval);
1899         }
1900
1901         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1902         $self->update_copy();
1903
1904     } else {
1905         $self->reservation->fine_amount(
1906             $self->reservation->target_resource_type->fine_amount
1907         );
1908         $self->reservation->max_fine(
1909             $self->reservation->target_resource_type->max_fine
1910         );
1911         $self->reservation->fine_interval(
1912             $self->reservation->target_resource_type->fine_interval
1913         );
1914     }
1915
1916     $self->update_reservation();
1917 }
1918
1919 sub do_reservation_return {
1920     my $self = shift;
1921     my $request = shift;
1922
1923     $self->log_me("do_reservation_return()");
1924
1925     if (not ref $self->reservation) {
1926         my ($reservation, $evt) =
1927             $U->fetch_booking_reservation($self->reservation);
1928         return $self->bail_on_events($evt) if $evt;
1929         $self->reservation($reservation);
1930     }
1931
1932     $self->handle_fines(1);
1933     $self->reservation->return_time('now');
1934     $self->update_reservation();
1935     $self->reshelve_copy if $self->copy;
1936
1937     if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1938         $self->copy( $self->reservation->current_resource->catalog_item );
1939     }
1940 }
1941
1942 sub booking_adjusted_due_date {
1943     my $self = shift;
1944     my $circ = $self->circ;
1945     my $copy = $self->copy;
1946
1947     return undef unless $self->use_booking;
1948
1949     my $changed;
1950
1951     if( $self->due_date ) {
1952
1953         return $self->bail_on_events($self->editor->event)
1954             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1955
1956        $circ->due_date(cleanse_ISO8601($self->due_date));
1957
1958     } else {
1959
1960         return unless $copy and $circ->due_date;
1961     }
1962
1963     my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1964     if (@$booking_items) {
1965         my $booking_item = $booking_items->[0];
1966         my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1967
1968         my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1969         my $shorten_circ_setting = $resource_type->elbow_room ||
1970             $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1971             '0 seconds';
1972
1973         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1974         my $bookings = $booking_ses->request('open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, {
1975               resource     => $booking_item->id
1976             , search_start => 'now'
1977             , search_end   => $circ->due_date
1978             , fields       => { cancel_time => undef, return_time => undef }
1979         })->gather(1);
1980         $booking_ses->disconnect;
1981
1982         throw OpenSRF::EX::ERROR ("Improper input arguments") unless defined $bookings;
1983         return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
1984         
1985         my $dt_parser = DateTime::Format::ISO8601->new;
1986         my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1987
1988         for my $bid (@$bookings) {
1989
1990             my $booking = $self->editor->retrieve_booking_reservation( $bid );
1991
1992             my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1993             my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1994
1995             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1996                 if ($booking_start < DateTime->now);
1997
1998
1999             if ($U->is_true($stop_circ_setting)) {
2000                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
2001             } else {
2002                 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2003                 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
2004             }
2005             
2006             # We set the circ duration here only to affect the logic that will
2007             # later (in a DB trigger) mangle the time part of the due date to
2008             # 11:59pm. Having any circ duration that is not a whole number of
2009             # days is enough to prevent the "correction."
2010             my $new_circ_duration = $due_date->epoch - time;
2011             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2012             $circ->duration("$new_circ_duration seconds");
2013
2014             $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2015             $changed = 1;
2016         }
2017
2018         return $self->bail_on_events($self->editor->event)
2019             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2020     }
2021
2022     return $changed;
2023 }
2024
2025 sub apply_modified_due_date {
2026     my $self = shift;
2027     my $shift_earlier = shift;
2028     my $circ = $self->circ;
2029     my $copy = $self->copy;
2030
2031    if( $self->due_date ) {
2032
2033         return $self->bail_on_events($self->editor->event)
2034             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2035
2036       $circ->due_date(cleanse_ISO8601($self->due_date));
2037
2038    } else {
2039
2040       # if the due_date lands on a day when the location is closed
2041       return unless $copy and $circ->due_date;
2042
2043         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2044
2045         # due-date overlap should be determined by the location the item
2046         # is checked out from, not the owning or circ lib of the item
2047         my $org = $self->circ_lib;
2048
2049       $logger->info("circulator: circ searching for closed date overlap on lib $org".
2050             " with an item due date of ".$circ->due_date );
2051
2052       my $dateinfo = $U->storagereq(
2053          'open-ils.storage.actor.org_unit.closed_date.overlap', 
2054             $org, $circ->due_date );
2055
2056       if($dateinfo) {
2057          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2058             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2059
2060             # XXX make the behavior more dynamic
2061             # for now, we just push the due date to after the close date
2062             if ($shift_earlier) {
2063                 $circ->due_date($dateinfo->{start});
2064             } else {
2065                 $circ->due_date($dateinfo->{end});
2066             }
2067       }
2068    }
2069 }
2070
2071
2072
2073 sub create_due_date {
2074     my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2075
2076     # if there is a raw time component (e.g. from postgres), 
2077     # turn it into an interval that interval_to_seconds can parse
2078     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2079
2080     # for now, use the server timezone.  TODO: use workstation org timezone
2081     my $due_date = DateTime->now(time_zone => 'local');
2082     $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2083
2084     # add the circ duration
2085     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2086
2087     if($date_ceiling) {
2088         my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2089         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2090             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2091             $due_date = $cdate;
2092         }
2093     }
2094
2095     # return ISO8601 time with timezone
2096     return $due_date->strftime('%FT%T%z');
2097 }
2098
2099
2100
2101 sub make_precat_copy {
2102     my $self = shift;
2103     my $copy = $self->copy;
2104
2105    if($copy) {
2106         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2107
2108         $copy->editor($self->editor->requestor->id);
2109         $copy->edit_date('now');
2110         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2111         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2112         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2113         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2114         $self->update_copy();
2115         return;
2116    }
2117
2118     $logger->info("circulator: Creating a new precataloged ".
2119         "copy in checkout with barcode " . $self->copy_barcode);
2120
2121     $copy = Fieldmapper::asset::copy->new;
2122     $copy->circ_lib($self->circ_lib);
2123     $copy->creator($self->editor->requestor->id);
2124     $copy->editor($self->editor->requestor->id);
2125     $copy->barcode($self->copy_barcode);
2126     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
2127     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2128     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2129
2130     $copy->dummy_title($self->dummy_title || "");
2131     $copy->dummy_author($self->dummy_author || "");
2132     $copy->dummy_isbn($self->dummy_isbn || "");
2133     $copy->circ_modifier($self->circ_modifier);
2134
2135
2136     # See if we need to override the circ_lib for the copy with a configured circ_lib
2137     # Setting is shortname of the org unit
2138     my $precat_circ_lib = $U->ou_ancestor_setting_value(
2139         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2140
2141     if($precat_circ_lib) {
2142         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2143
2144         if(!$org) {
2145             $self->bail_on_events($self->editor->event);
2146             return;
2147         }
2148
2149         $copy->circ_lib($org->id);
2150     }
2151
2152
2153     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2154         $self->bail_out(1);
2155         $self->push_events($self->editor->event);
2156         return;
2157     }   
2158 }
2159
2160
2161 sub checkout_noncat {
2162     my $self = shift;
2163
2164     my $circ;
2165     my $evt;
2166
2167    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
2168    my $count    = $self->noncat_count || 1;
2169    my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
2170
2171    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2172
2173    for(1..$count) {
2174
2175       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2176          $self->editor->requestor->id, 
2177             $self->patron->id, 
2178             $lib, 
2179             $self->noncat_type, 
2180             $cotime,
2181             $self->editor );
2182
2183         if( $evt ) {
2184             $self->push_events($evt);
2185             $self->bail_out(1);
2186             return; 
2187         }
2188         $self->circ($circ);
2189    }
2190 }
2191
2192 # If a copy goes into transit and is then checked in before the transit checkin 
2193 # interval has expired, push an event onto the overridable events list.
2194 sub check_transit_checkin_interval {
2195     my $self = shift;
2196
2197     # only concerned with in-transit items
2198     return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2199
2200     # no interval, no problem
2201     my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2202     return unless $interval;
2203
2204     # capture the transit so we don't have to fetch it again later during checkin
2205     $self->transit(
2206         $self->editor->search_action_transit_copy(
2207             {target_copy => $self->copy->id, dest_recv_time => undef}
2208         )->[0]
2209     ); 
2210
2211     # transit from X to X for whatever reason has no min interval
2212     return if $self->transit->source == $self->transit->dest;
2213
2214     my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2215     my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2216     my $horizon = $t_start->add(seconds => $seconds);
2217
2218     # See if we are still within the transit checkin forbidden range
2219     $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK')) 
2220         if $horizon > DateTime->now;
2221 }
2222
2223 # Retarget local holds at checkin
2224 sub checkin_retarget {
2225     my $self = shift;
2226     return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2227     return unless $self->is_checkin; # Renewals need not be checked
2228     return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2229     return if $self->is_precat; # No holds for precats
2230     return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2231     return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2232     my $status = $U->copy_status($self->copy->status);
2233     return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2234     # Specifically target items that are likely new (by status ID)
2235     return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2236     my $location = $self->copy->location;
2237     if(!ref($location)) {
2238         $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2239         $self->copy->location($location);
2240     }
2241     return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2242
2243     # Fetch holds for the bib
2244     my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2245                     $self->editor->authtoken,
2246                     $self->title->id,
2247                     {
2248                         capture_time => undef, # No touching captured holds
2249                         frozen => 'f', # Don't bother with frozen holds
2250                         pickup_lib => $self->circ_lib # Only holds actually here
2251                     }); 
2252
2253     # Error? Skip the step.
2254     return if exists $result->{"ilsevent"};
2255
2256     # Assemble holds
2257     my $holds = [];
2258     foreach my $holdlist (keys %{$result}) {
2259         push @$holds, @{$result->{$holdlist}};
2260     }
2261
2262     return if scalar(@$holds) == 0; # No holds, no retargeting
2263
2264     # Check for parts on this copy
2265     my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2266     my %parts_hash = ();
2267     %parts_hash = map {$_->part, 1} @$parts if @$parts;
2268
2269     # Loop over holds in request-ish order
2270     # Stage 1: Get them into request-ish order
2271     # Also grab type and target for skipping low hanging ones
2272     $result = $self->editor->json_query({
2273         "select" => { "ahr" => ["id", "hold_type", "target"] },
2274         "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
2275         "where" => { "id" => $holds },
2276         "order_by" => [
2277             { "class" => "pgt", "field" => "hold_priority"},
2278             { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2279             { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2280             { "class" => "ahr", "field" => "request_time"}
2281         ]
2282     });
2283
2284     # Stage 2: Loop!
2285     if (ref $result eq "ARRAY" and scalar @$result) {
2286         foreach (@{$result}) {
2287             # Copy level, but not this copy?
2288             next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2289                 and $_->{target} != $self->copy->id);
2290             # Volume level, but not this volume?
2291             next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2292             if(@$parts) { # We have parts?
2293                 # Skip title holds
2294                 next if ($_->{hold_type} eq 'T');
2295                 # Skip part holds for parts not on this copy
2296                 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2297             } else {
2298                 # No parts, no part holds
2299                 next if ($_->{hold_type} eq 'P');
2300             }
2301             # So much for easy stuff, attempt a retarget!
2302             my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2303             if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2304                 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2305             }
2306         }
2307     }
2308 }
2309
2310 sub do_checkin {
2311     my $self = shift;
2312     $self->log_me("do_checkin()");
2313
2314     return $self->bail_on_events(
2315         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
2316         unless $self->copy;
2317
2318     $self->check_transit_checkin_interval;
2319     $self->checkin_retarget;
2320
2321     # the renew code and mk_env should have already found our circulation object
2322     unless( $self->circ ) {
2323
2324         my $circs = $self->editor->search_action_circulation(
2325             { target_copy => $self->copy->id, checkin_time => undef });
2326
2327         $self->circ($$circs[0]);
2328
2329         # for now, just warn if there are multiple open circs on a copy
2330         $logger->warn("circulator: we have ".scalar(@$circs).
2331             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2332     }
2333
2334     my $stat = $U->copy_status($self->copy->status)->id;
2335
2336     # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2337     # differently if they are already paid for.  We need to check for this
2338     # early since overdue generation is potentially affected.
2339     my $dont_change_lost_zero = 0;
2340     if ($stat == OILS_COPY_STATUS_LOST
2341         || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2342         || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2343
2344         # LOST fine settings are controlled by the copy's circ lib, not the the
2345         # circulation's
2346         my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2347                 $self->copy->circ_lib->id : $self->copy->circ_lib;
2348         $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2349             $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2350             $self->editor) || 0;
2351
2352         if ($dont_change_lost_zero) {
2353             my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2354             $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2355         }
2356
2357         $self->dont_change_lost_zero($dont_change_lost_zero);
2358     }
2359
2360     if( $self->checkin_check_holds_shelf() ) {
2361         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2362         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2363         if($self->fake_hold_dest) {
2364             $self->hold->pickup_lib($self->circ_lib);
2365         }
2366         $self->checkin_flesh_events;
2367         return;
2368     }
2369
2370     unless( $self->is_renewal ) {
2371         return $self->bail_on_events($self->editor->event)
2372             unless $self->editor->allowed('COPY_CHECKIN');
2373     }
2374
2375     $self->push_events($self->check_copy_alert());
2376     $self->push_events($self->check_checkin_copy_status());
2377
2378     # if the circ is marked as 'claims returned', add the event to the list
2379     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2380         if ($self->circ and $self->circ->stop_fines 
2381                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2382
2383     $self->check_circ_deposit();
2384
2385     # handle the overridable events 
2386     $self->override_events unless $self->is_renewal;
2387     return if $self->bail_out;
2388     
2389     if( $self->copy and !$self->transit ) {
2390         $self->transit(
2391             $self->editor->search_action_transit_copy(
2392                 { target_copy => $self->copy->id, dest_recv_time => undef }
2393             )->[0]
2394         ); 
2395     }
2396
2397     if( $self->circ ) {
2398         $self->checkin_handle_circ_start;
2399         return if $self->bail_out;
2400
2401         if (!$dont_change_lost_zero) {
2402             # if this circ is LOST and we are configured to generate overdue
2403             # fines for lost items on checkin (to fill the gap between mark
2404             # lost time and when the fines would have naturally stopped), then
2405             # stop_fines is no longer valid and should be cleared.
2406             #
2407             # stop_fines will be set again during the handle_fines() stage.
2408             # XXX should this setting come from the copy circ lib (like other
2409             # LOST settings), instead of the circulation circ lib?
2410             if ($stat == OILS_COPY_STATUS_LOST) {
2411                 $self->circ->clear_stop_fines if
2412                     $U->ou_ancestor_setting_value(
2413                         $self->circ_lib,
2414                         OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2415                         $self->editor
2416                     );
2417             }
2418
2419             # handle fines for this circ, including overdue gen if needed
2420             $self->handle_fines;
2421         }
2422
2423         $self->checkin_handle_circ_finish;
2424         return if $self->bail_out;
2425         $self->checkin_changed(1);
2426
2427     } elsif( $self->transit ) {
2428         my $hold_transit = $self->process_received_transit;
2429         $self->checkin_changed(1);
2430
2431         if( $self->bail_out ) { 
2432             $self->checkin_flesh_events;
2433             return;
2434         }
2435         
2436         if( my $e = $self->check_checkin_copy_status() ) {
2437             # If the original copy status is special, alert the caller
2438             my $ev = $self->events;
2439             $self->events([$e]);
2440             $self->override_events;
2441             return if $self->bail_out;
2442             $self->events($ev);
2443         }
2444
2445         if( $hold_transit or 
2446                 $U->copy_status($self->copy->status)->id 
2447                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2448
2449             my $hold;
2450             if( $hold_transit ) {
2451                $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2452             } else {
2453                    ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2454             }
2455
2456             $self->hold($hold);
2457
2458             if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2459
2460                 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2461                 $self->reshelve_copy(1);
2462                 $self->cancelled_hold_transit(1);
2463                 $self->notify_hold(0); # don't notify for cancelled holds
2464                 $self->fake_hold_dest(0);
2465                 return if $self->bail_out;
2466
2467             } elsif ($hold and $hold->hold_type eq 'R') {
2468
2469                 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2470                 $self->notify_hold(0); # No need to notify
2471                 $self->fake_hold_dest(0);
2472                 $self->noop(1); # Don't try and capture for other holds/transits now
2473                 $self->update_copy();
2474                 $hold->fulfillment_time('now');
2475                 $self->bail_on_events($self->editor->event)
2476                     unless $self->editor->update_action_hold_request($hold);
2477
2478             } else {
2479
2480                 # hold transited to correct location
2481                 if($self->fake_hold_dest) {
2482                     $hold->pickup_lib($self->circ_lib);
2483                 }
2484                 $self->checkin_flesh_events;
2485                 return;
2486             }
2487         } 
2488
2489     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2490
2491         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2492             " that is in-transit, but there is no transit.. repairing");
2493         $self->reshelve_copy(1);
2494         return if $self->bail_out;
2495     }
2496
2497     if( $self->is_renewal ) {
2498         $self->finish_fines_and_voiding;
2499         return if $self->bail_out;
2500         $self->push_events(OpenILS::Event->new('SUCCESS'));
2501         return;
2502     }
2503
2504    # ------------------------------------------------------------------------------
2505    # Circulations and transits are now closed where necessary.  Now go on to see if
2506    # this copy can fulfill a hold or needs to be routed to a different location
2507    # ------------------------------------------------------------------------------
2508
2509     my $needed_for_something = 0; # formerly "needed_for_hold"
2510
2511     if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2512
2513         if (!$self->remote_hold) {
2514             if ($self->use_booking) {
2515                 my $potential_hold = $self->hold_capture_is_possible;
2516                 my $potential_reservation = $self->reservation_capture_is_possible;
2517
2518                 if ($potential_hold and $potential_reservation) {
2519                     $logger->info("circulator: item could fulfill either hold or reservation");
2520                     $self->push_events(new OpenILS::Event(
2521                         "HOLD_RESERVATION_CONFLICT",
2522                         "hold" => $potential_hold,
2523                         "reservation" => $potential_reservation
2524                     ));
2525                     return if $self->bail_out;
2526                 } elsif ($potential_hold) {
2527                     $needed_for_something =
2528                         $self->attempt_checkin_hold_capture;
2529                 } elsif ($potential_reservation) {
2530                     $needed_for_something =
2531                         $self->attempt_checkin_reservation_capture;
2532                 }
2533             } else {
2534                 $needed_for_something = $self->attempt_checkin_hold_capture;
2535             }
2536         }
2537         return if $self->bail_out;
2538     
2539         unless($needed_for_something) {
2540             my $circ_lib = (ref $self->copy->circ_lib) ? 
2541                     $self->copy->circ_lib->id : $self->copy->circ_lib;
2542     
2543             if( $self->remote_hold ) {
2544                 $circ_lib = $self->remote_hold->pickup_lib;
2545                 $logger->warn("circulator: Copy ".$self->copy->barcode.
2546                     " is on a remote hold's shelf, sending to $circ_lib");
2547             }
2548     
2549             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2550
2551             my $suppress_transit = 0;
2552
2553             if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2554                 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2555                 if($suppress_transit_source && $suppress_transit_source->{value}) {
2556                     my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2557                     if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2558                         $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2559                         $suppress_transit = 1;
2560                     }
2561                 }
2562             }
2563  
2564             if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2565                 # copy is where it needs to be, either for hold or reshelving
2566     
2567                 $self->checkin_handle_precat();
2568                 return if $self->bail_out;
2569     
2570             } else {
2571                 # copy needs to transit "home", or stick here if it's a floating copy
2572                 my $can_float = 0;
2573                 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2574                     my $res = $self->editor->json_query(
2575                         {   from => [
2576                                 'evergreen.can_float',
2577                                 $self->copy->floating->id,
2578                                 $self->copy->circ_lib,
2579                                 $self->circ_lib
2580                             ]
2581                         }
2582                     );
2583                     $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res; 
2584                 }
2585                 if ($can_float) { # Yep, floating, stick here
2586                     $self->checkin_changed(1);
2587                     $self->copy->circ_lib( $self->circ_lib );
2588                     $self->update_copy;
2589                 } else {
2590                     my $bc = $self->copy->barcode;
2591                     $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2592                     $self->checkin_build_copy_transit($circ_lib);
2593                     return if $self->bail_out;
2594                     $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2595                 }
2596             }
2597         }
2598     } else { # no-op checkin
2599         if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2600             $self->checkin_changed(1);
2601             $self->copy->circ_lib( $self->circ_lib );
2602             $self->update_copy;
2603         }
2604     }
2605
2606     if($self->claims_never_checked_out and 
2607             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2608
2609         # the item was not supposed to be checked out to the user and should now be marked as missing
2610         $self->copy->status(OILS_COPY_STATUS_MISSING);
2611         $self->update_copy;
2612
2613     } else {
2614         $self->reshelve_copy unless $needed_for_something;
2615     }
2616
2617     return if $self->bail_out;
2618
2619     unless($self->checkin_changed) {
2620
2621         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2622         my $stat = $U->copy_status($self->copy->status)->id;
2623
2624         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2625          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2626         $self->bail_out(1); # no need to commit anything
2627
2628     } else {
2629
2630         $self->push_events(OpenILS::Event->new('SUCCESS')) 
2631             unless @{$self->events};
2632     }
2633
2634     $self->finish_fines_and_voiding;
2635
2636     OpenILS::Utils::Penalty->calculate_penalties(
2637         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2638
2639     $self->checkin_flesh_events;
2640     return;
2641 }
2642
2643 sub finish_fines_and_voiding {
2644     my $self = shift;
2645     return unless $self->circ;
2646
2647     return unless $self->backdate or $self->void_overdues;
2648
2649     # void overdues after fine generation to prevent concurrent DB access to overdue billings
2650     my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2651
2652     my $evt = $CC->void_or_zero_overdues(
2653         $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
2654
2655     return $self->bail_on_events($evt) if $evt;
2656
2657     # Make sure the circ is open or closed as necessary.
2658     $evt = $U->check_open_xact($self->editor, $self->circ->id);
2659     return $self->bail_on_events($evt) if $evt;
2660
2661     return undef;
2662 }
2663
2664
2665 # if a deposit was payed for this item, push the event
2666 sub check_circ_deposit {
2667     my $self = shift;
2668     return unless $self->circ;
2669     my $deposit = $self->editor->search_money_billing(
2670         {   btype => 5, 
2671             xact => $self->circ->id, 
2672             voided => 'f'
2673         }, {idlist => 1})->[0];
2674
2675     $self->push_events(OpenILS::Event->new(
2676         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2677 }
2678
2679 sub reshelve_copy {
2680    my $self    = shift;
2681    my $force   = $self->force || shift;
2682    my $copy    = $self->copy;
2683
2684    my $stat = $U->copy_status($copy->status)->id;
2685
2686    if($force || (
2687       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2688       $stat != OILS_COPY_STATUS_CATALOGING and
2689       $stat != OILS_COPY_STATUS_IN_TRANSIT and
2690       $stat != OILS_COPY_STATUS_RESHELVING  )) {
2691
2692         $copy->status( OILS_COPY_STATUS_RESHELVING );
2693             $self->update_copy;
2694             $self->checkin_changed(1);
2695     }
2696 }
2697
2698
2699 # Returns true if the item is at the current location
2700 # because it was transited there for a hold and the 
2701 # hold has not been fulfilled
2702 sub checkin_check_holds_shelf {
2703     my $self = shift;
2704     return 0 unless $self->copy;
2705
2706     return 0 unless 
2707         $U->copy_status($self->copy->status)->id ==
2708             OILS_COPY_STATUS_ON_HOLDS_SHELF;
2709
2710     # Attempt to clear shelf expired holds for this copy
2711     $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2712         if($self->clear_expired);
2713
2714     # find the hold that put us on the holds shelf
2715     my $holds = $self->editor->search_action_hold_request(
2716         { 
2717             current_copy => $self->copy->id,
2718             capture_time => { '!=' => undef },
2719             fulfillment_time => undef,
2720             cancel_time => undef,
2721         }
2722     );
2723
2724     unless(@$holds) {
2725         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2726         $self->reshelve_copy(1);
2727         return 0;
2728     }
2729
2730     my $hold = $$holds[0];
2731
2732     $logger->info("circulator: we found a captured, un-fulfilled hold [".
2733         $hold->id. "] for copy ".$self->copy->barcode);
2734
2735     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2736         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2737         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2738             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2739             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2740                 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2741                 $self->fake_hold_dest(1);
2742                 return 1;
2743             }
2744         }
2745     }
2746
2747     if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2748         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2749         return 1;
2750     }
2751
2752     $logger->info("circulator: hold is not for here..");
2753     $self->remote_hold($hold);
2754     return 0;
2755 }
2756
2757
2758 sub checkin_handle_precat {
2759     my $self    = shift;
2760    my $copy    = $self->copy;
2761
2762    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2763         $copy->status(OILS_COPY_STATUS_CATALOGING);
2764         $self->update_copy();
2765         $self->checkin_changed(1);
2766         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2767    }
2768 }
2769
2770
2771 sub checkin_build_copy_transit {
2772     my $self            = shift;
2773     my $dest            = shift;
2774     my $copy       = $self->copy;
2775     my $transit    = Fieldmapper::action::transit_copy->new;
2776
2777     # if we are transiting an item to the shelf shelf, it's a hold transit
2778     if (my $hold = $self->remote_hold) {
2779         $transit = Fieldmapper::action::hold_transit_copy->new;
2780         $transit->hold($hold->id);
2781
2782         # the item is going into transit, remove any shelf-iness
2783         if ($hold->current_shelf_lib or $hold->shelf_time) {
2784             $hold->clear_current_shelf_lib;
2785             $hold->clear_shelf_time;
2786             return $self->bail_on_events($self->editor->event)
2787                 unless $self->editor->update_action_hold_request($hold);
2788         }
2789     }
2790
2791     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2792     $logger->info("circulator: transiting copy to $dest");
2793
2794     $transit->source($self->circ_lib);
2795     $transit->dest($dest);
2796     $transit->target_copy($copy->id);
2797     $transit->source_send_time('now');
2798     $transit->copy_status( $U->copy_status($copy->status)->id );
2799
2800     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2801
2802     if ($self->remote_hold) {
2803         return $self->bail_on_events($self->editor->event)
2804             unless $self->editor->create_action_hold_transit_copy($transit);
2805     } else {
2806         return $self->bail_on_events($self->editor->event)
2807             unless $self->editor->create_action_transit_copy($transit);
2808     }
2809
2810     # ensure the transit is returned to the caller
2811     $self->transit($transit);
2812
2813     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2814     $self->update_copy;
2815     $self->checkin_changed(1);
2816 }
2817
2818
2819 sub hold_capture_is_possible {
2820     my $self = shift;
2821     my $copy = $self->copy;
2822
2823     # we've been explicitly told not to capture any holds
2824     return 0 if $self->capture eq 'nocapture';
2825
2826     # See if this copy can fulfill any holds
2827     my $hold = $holdcode->find_nearest_permitted_hold(
2828         $self->editor, $copy, $self->editor->requestor, 1 # check_only
2829     );
2830     return undef if ref $hold eq "HASH" and
2831         $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2832     return $hold;
2833 }
2834
2835 sub reservation_capture_is_possible {
2836     my $self = shift;
2837     my $copy = $self->copy;
2838
2839     # we've been explicitly told not to capture any holds
2840     return 0 if $self->capture eq 'nocapture';
2841
2842     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2843     my $resv = $booking_ses->request(
2844         "open-ils.booking.reservations.could_capture",
2845         $self->editor->authtoken, $copy->barcode
2846     )->gather(1);
2847     $booking_ses->disconnect;
2848     if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2849         $self->push_events($resv);
2850     } else {
2851         return $resv;
2852     }
2853 }
2854
2855 # returns true if the item was used (or may potentially be used 
2856 # in subsequent calls) to capture a hold.
2857 sub attempt_checkin_hold_capture {
2858     my $self = shift;
2859     my $copy = $self->copy;
2860
2861     # we've been explicitly told not to capture any holds
2862     return 0 if $self->capture eq 'nocapture';
2863
2864     # See if this copy can fulfill any holds
2865     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2866         $self->editor, $copy, $self->editor->requestor );
2867
2868     if(!$hold) {
2869         $logger->debug("circulator: no potential permitted".
2870             "holds found for copy ".$copy->barcode);
2871         return 0;
2872     }
2873
2874     if($self->capture ne 'capture') {
2875         # see if this item is in a hold-capture-delay location
2876         my $location = $self->copy->location;
2877         if(!ref($location)) {
2878             $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2879             $self->copy->location($location);
2880         }
2881         if($U->is_true($location->hold_verify)) {
2882             $self->bail_on_events(
2883                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2884             return 1;
2885         }
2886     }
2887
2888     $self->retarget($retarget);
2889
2890     my $suppress_transit = 0;
2891     if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2892         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2893         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2894             my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2895             if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2896                 $suppress_transit = 1;
2897                 $hold->pickup_lib($self->circ_lib);
2898             }
2899         }
2900     }
2901
2902     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2903
2904     $hold->current_copy($copy->id);
2905     $hold->capture_time('now');
2906     $self->put_hold_on_shelf($hold) 
2907         if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2908
2909     # prevent DB errors caused by fetching 
2910     # holds from storage, and updating through cstore
2911     $hold->clear_fulfillment_time;
2912     $hold->clear_fulfillment_staff;
2913     $hold->clear_fulfillment_lib;
2914     $hold->clear_expire_time; 
2915     $hold->clear_cancel_time;
2916     $hold->clear_prev_check_time unless $hold->prev_check_time;
2917
2918     $self->bail_on_events($self->editor->event)
2919         unless $self->editor->update_action_hold_request($hold);
2920     $self->hold($hold);
2921     $self->checkin_changed(1);
2922
2923     return 0 if $self->bail_out;
2924
2925     if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2926
2927         if ($hold->hold_type eq 'R') {
2928             $copy->status(OILS_COPY_STATUS_CATALOGING);
2929             $hold->fulfillment_time('now');
2930             $self->noop(1); # Block other transit/hold checks
2931             $self->bail_on_events($self->editor->event)
2932                 unless $self->editor->update_action_hold_request($hold);
2933         } else {
2934             # This hold was captured in the correct location
2935             $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2936             $self->push_events(OpenILS::Event->new('SUCCESS'));
2937
2938             #$self->do_hold_notify($hold->id);
2939             $self->notify_hold($hold->id);
2940         }
2941
2942     } else {
2943     
2944         # Hold needs to be picked up elsewhere.  Build a hold
2945         # transit and route the item.
2946         $self->checkin_build_hold_transit();
2947         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2948         return 0 if $self->bail_out;
2949         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2950     }
2951
2952     # make sure we save the copy status
2953     $self->update_copy;
2954     return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
2955     return 1;
2956 }
2957
2958 sub attempt_checkin_reservation_capture {
2959     my $self = shift;
2960     my $copy = $self->copy;
2961
2962     # we've been explicitly told not to capture any holds
2963     return 0 if $self->capture eq 'nocapture';
2964
2965     my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2966     my $evt = $booking_ses->request(
2967         "open-ils.booking.resources.capture_for_reservation",
2968         $self->editor->authtoken,
2969         $copy->barcode,
2970         1 # don't update copy - we probably have it locked
2971     )->gather(1);
2972     $booking_ses->disconnect;
2973
2974     if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2975         $logger->warn(
2976             "open-ils.booking.resources.capture_for_reservation " .
2977             "didn't return an event!"
2978         );
2979     } else {
2980         if (
2981             $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2982             $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2983         ) {
2984             # not-transferable is an error event we'll pass on the user
2985             $logger->warn("reservation capture attempted against non-transferable item");
2986             $self->push_events($evt);
2987             return 0;
2988         } elsif ($evt->{"textcode"} eq "SUCCESS") {
2989             # Re-retrieve copy as reservation capture may have changed
2990             # its status and whatnot.
2991             $logger->info(
2992                 "circulator: booking capture win on copy " . $self->copy->id
2993             );
2994             if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2995                 $logger->info(
2996                     "circulator: changing copy " . $self->copy->id .
2997                     "'s status from " . $self->copy->status . " to " .
2998                     $new_copy_status
2999                 );
3000                 $self->copy->status($new_copy_status);
3001                 $self->update_copy;
3002             }
3003             $self->reservation($evt->{"payload"}->{"reservation"});
3004
3005             if (exists $evt->{"payload"}->{"transit"}) {
3006                 $self->push_events(
3007                     new OpenILS::Event(
3008                         "ROUTE_ITEM",
3009                         "org" => $evt->{"payload"}->{"transit"}->dest
3010                     )
3011                 );
3012             }
3013             $self->checkin_changed(1);
3014             return 1;
3015         }
3016     }
3017     # other results are treated as "nothing to capture"
3018     return 0;
3019 }
3020
3021 sub do_hold_notify {
3022     my( $self, $holdid ) = @_;
3023
3024     my $e = new_editor(xact => 1);
3025     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3026     $e->rollback;
3027     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3028     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3029
3030     $logger->info("circulator: running delayed hold notify process");
3031
3032 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3033 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3034
3035     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3036         hold_id => $holdid, requestor => $self->editor->requestor);
3037
3038     $logger->debug("circulator: built hold notifier");
3039
3040     if(!$notifier->event) {
3041
3042         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3043
3044         my $stat = $notifier->send_email_notify;
3045         if( $stat == '1' ) {
3046             $logger->info("circulator: hold notify succeeded for hold $holdid");
3047             return;
3048         } 
3049
3050         $logger->debug("circulator:  * hold notify cancelled or failed for hold $holdid");
3051
3052     } else {
3053         $logger->info("circulator: Not sending hold notification since the patron has no email address");
3054     }
3055 }
3056
3057 sub retarget_holds {
3058     my $self = shift;
3059     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3060     my $ses = OpenSRF::AppSession->create('open-ils.storage');
3061     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3062     # no reason to wait for the return value
3063     return;
3064 }
3065
3066 sub checkin_build_hold_transit {
3067     my $self = shift;
3068
3069    my $copy = $self->copy;
3070    my $hold = $self->hold;
3071    my $trans = Fieldmapper::action::hold_transit_copy->new;
3072
3073     $logger->debug("circulator: building hold transit for ".$copy->barcode);
3074
3075    $trans->hold($hold->id);
3076    $trans->source($self->circ_lib);
3077    $trans->dest($hold->pickup_lib);
3078    $trans->source_send_time("now");
3079    $trans->target_copy($copy->id);
3080
3081     # when the copy gets to its destination, it will recover
3082     # this status - put it onto the holds shelf
3083    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3084
3085     return $self->bail_on_events($self->editor->event)
3086         unless $self->editor->create_action_hold_transit_copy($trans);
3087 }
3088
3089
3090
3091 sub process_received_transit {
3092     my $self = shift;
3093     my $copy = $self->copy;
3094     my $copyid = $self->copy->id;
3095
3096     my $status_name = $U->copy_status($copy->status)->name;
3097     $logger->debug("circulator: attempting transit receive on ".
3098         "copy $copyid. Copy status is $status_name");
3099
3100     my $transit = $self->transit;
3101
3102     # Check if we are in a transit suppress range
3103     my $suppress_transit = 0;
3104     if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3105         my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ?  'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3106         my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3107         if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3108             my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3109             if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3110                 $suppress_transit = 1;
3111                 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3112             }
3113         }
3114     }
3115     if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3116         # - this item is in-transit to a different location
3117         # - Or we are capturing holds as transits, so why create a new transit?
3118
3119         my $tid = $transit->id; 
3120         my $loc = $self->circ_lib;
3121         my $dest = $transit->dest;
3122
3123         $logger->info("circulator: Fowarding transit on copy which is destined ".
3124             "for a different location. transit=$tid, copy=$copyid, current ".
3125             "location=$loc, destination location=$dest");
3126
3127         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3128
3129         # grab the associated hold object if available
3130         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3131         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3132
3133         return $self->bail_on_events($evt);
3134     }
3135
3136     # The transit is received, set the receive time
3137     $transit->dest_recv_time('now');
3138     $self->bail_on_events($self->editor->event)
3139         unless $self->editor->update_action_transit_copy($transit);
3140
3141     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3142
3143     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3144     $copy->status( $transit->copy_status );
3145     $self->update_copy();
3146     return if $self->bail_out;
3147
3148     my $ishold = 0;
3149     if($hold_transit) { 
3150         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3151
3152         if ($hold) {
3153             # hold has arrived at destination, set shelf time
3154             $self->put_hold_on_shelf($hold);
3155             $self->bail_on_events($self->editor->event)
3156                 unless $self->editor->update_action_hold_request($hold);
3157             return if $self->bail_out;
3158
3159             $self->notify_hold($hold_transit->hold);
3160             $ishold = 1;
3161         } else {
3162             $hold_transit = undef;
3163             $self->cancelled_hold_transit(1);
3164             $self->reshelve_copy(1);
3165             $self->fake_hold_dest(0);
3166         }
3167     }
3168
3169     $self->push_events( 
3170         OpenILS::Event->new(
3171         'SUCCESS', 
3172         ishold => $ishold,
3173       payload => { transit => $transit, holdtransit => $hold_transit } ));
3174
3175     return $hold_transit;
3176 }
3177
3178
3179 # ------------------------------------------------------------------
3180 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3181 # ------------------------------------------------------------------
3182 sub put_hold_on_shelf {
3183     my($self, $hold) = @_;
3184     $hold->shelf_time('now');
3185     $hold->current_shelf_lib($self->circ_lib);
3186     $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3187     return undef;
3188 }
3189
3190 sub handle_fines {
3191    my $self = shift;
3192    my $reservation = shift;
3193    my $dt_parser = DateTime::Format::ISO8601->new;
3194
3195    my $obj = $reservation ? $self->reservation : $self->circ;
3196
3197     my $lost_bill_opts = $self->lost_bill_options;
3198     my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3199     # first, restore any voided overdues for lost, if needed
3200     if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3201         my $restore_od = $U->ou_ancestor_setting_value(
3202             $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3203             $self->editor) || 0;
3204         $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3205             if $restore_od;
3206     }
3207
3208     # next, handle normal overdue generation and apply stop_fines
3209     # XXX reservations don't have stop_fines
3210     # TODO revisit booking_reservation re: stop_fines support
3211     if ($reservation or !$obj->stop_fines) {
3212         my $skip_for_grace;
3213
3214         # This is a crude check for whether we are in a grace period. The code
3215         # in generate_fines() does a more thorough job, so this exists solely
3216         # as a small optimization, and might be better off removed.
3217
3218         # If we have a grace period
3219         if($obj->can('grace_period')) {
3220             # Parse out the due date
3221             my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3222             # Add the grace period to the due date
3223             $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3224             # Don't generate fines on circs still in grace period
3225             $skip_for_grace = $due_date > DateTime->now;
3226         }
3227         $CC->generate_fines({circs => [$obj], editor => $self->editor})
3228             unless $skip_for_grace;
3229
3230         if (!$reservation and !$obj->stop_fines) {
3231             $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3232             $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3233             $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3234             $obj->stop_fines_time('now');
3235             $obj->stop_fines_time($self->backdate) if $self->backdate;
3236             $self->editor->update_action_circulation($obj);
3237         }
3238     }
3239
3240     # finally, handle voiding of lost item and processing fees
3241     if ($self->needs_lost_bill_handling) {
3242         my $void_cost = $U->ou_ancestor_setting_value(
3243             $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3244             $self->editor) || 0;
3245         my $void_proc_fee = $U->ou_ancestor_setting_value(
3246             $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3247             $self->editor) || 0;
3248         $self->checkin_handle_lost_or_lo_now_found(
3249             $lost_bill_opts->{void_cost_btype},
3250             $lost_bill_opts->{is_longoverdue}) if $void_cost;
3251         $self->checkin_handle_lost_or_lo_now_found(
3252             $lost_bill_opts->{void_fee_btype},
3253             $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3254     }
3255
3256    return undef;
3257 }
3258
3259 sub checkin_handle_circ_start {
3260    my $self = shift;
3261    my $circ = $self->circ;
3262    my $copy = $self->copy;
3263    my $evt;
3264    my $obt;
3265
3266    $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3267
3268    # backdate the circ if necessary
3269    if($self->backdate) {
3270         my $evt = $self->checkin_handle_backdate;
3271         return $self->bail_on_events($evt) if $evt;
3272    }
3273
3274     # Set the checkin vars since we have the item
3275     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3276
3277     # capture the true scan time for back-dated checkins
3278     $circ->checkin_scan_time('now');
3279
3280     $circ->checkin_staff($self->editor->requestor->id);
3281     $circ->checkin_lib($self->circ_lib);
3282     $circ->checkin_workstation($self->editor->requestor->wsid);
3283
3284     my $circ_lib = (ref $self->copy->circ_lib) ?  
3285         $self->copy->circ_lib->id : $self->copy->circ_lib;
3286     my $stat = $U->copy_status($self->copy->status)->id;
3287
3288     if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3289         # we will now handle lost fines, but the copy will retain its 'lost'
3290         # status if it needs to transit home unless lost_immediately_available
3291         # is true
3292         #
3293         # if we decide to also delay fine handling until the item arrives home,
3294         # we will need to call lost fine handling code both when checking items
3295         # in and also when receiving transits
3296         $self->checkin_handle_lost($circ_lib);
3297     } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3298         # same process as above.
3299         $self->checkin_handle_long_overdue($circ_lib);
3300     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3301         $logger->info("circulator: not updating copy status on checkin because copy is missing");
3302     } else {
3303         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3304         $self->update_copy;
3305     }
3306
3307     return undef;
3308 }
3309
3310 sub checkin_handle_circ_finish {
3311     my $self = shift;
3312     my $circ = $self->circ;
3313
3314     # see if there are any fines owed on this circ.  if not, close it
3315     my ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3316     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3317
3318     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3319
3320     return $self->bail_on_events($self->editor->event)
3321         unless $self->editor->update_action_circulation($circ);
3322
3323     return undef;
3324 }
3325
3326 # ------------------------------------------------------------------
3327 # See if we need to void billings, etc. for lost checkin
3328 # ------------------------------------------------------------------
3329 sub checkin_handle_lost {
3330     my $self = shift;
3331     my $circ_lib = shift;
3332
3333     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3334         OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3335
3336     $self->lost_bill_options({
3337         circ_lib => $circ_lib,
3338         ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3339         ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3340         ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3341         void_cost_btype => 3, 
3342         void_fee_btype => 4 
3343     });
3344
3345     return $self->checkin_handle_lost_or_longoverdue(
3346         circ_lib => $circ_lib,
3347         max_return => $max_return,
3348         ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3349         ous_use_last_activity => undef # not supported for LOST checkin
3350     );
3351 }
3352
3353 # ------------------------------------------------------------------
3354 # See if we need to void billings, etc. for long-overdue checkin
3355 # note: not using constants below since they serve little purpose 
3356 # for single-use strings that are descriptive in their own right 
3357 # and mostly just complicate debugging.
3358 # ------------------------------------------------------------------
3359 sub checkin_handle_long_overdue {
3360     my $self = shift;
3361     my $circ_lib = shift;
3362
3363     $logger->info("circulator: processing long-overdue checkin...");
3364
3365     my $max_return = $U->ou_ancestor_setting_value($circ_lib, 
3366         'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3367
3368     $self->lost_bill_options({
3369         circ_lib => $circ_lib,
3370         ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3371         ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3372         is_longoverdue => 1,
3373         ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3374         void_cost_btype => 10,
3375         void_fee_btype => 11
3376     });
3377
3378     return $self->checkin_handle_lost_or_longoverdue(
3379         circ_lib => $circ_lib,
3380         max_return => $max_return,
3381         ous_immediately_available => 'circ.longoverdue_immediately_available',
3382         ous_use_last_activity => 
3383             'circ.longoverdue.use_last_activity_date_on_return'
3384     )
3385 }
3386
3387 # last billing activity is last payment time, last billing time, or the 
3388 # circ due date.  If the relevant "use last activity" org unit setting is 
3389 # false/unset, then last billing activity is always the due date.
3390 sub get_circ_last_billing_activity {
3391     my $self = shift;
3392     my $circ_lib = shift;
3393     my $setting = shift;
3394     my $date = $self->circ->due_date;
3395
3396     return $date unless $setting and 
3397         $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3398
3399     my $xact = $self->editor->retrieve_money_billable_transaction([
3400         $self->circ->id,
3401         {flesh => 1, flesh_fields => {mbt => ['summary']}}
3402     ]);
3403
3404     if ($xact->summary) {
3405         $date = $xact->summary->last_payment_ts || 
3406                 $xact->summary->last_billing_ts || 
3407                 $self->circ->due_date;
3408     }
3409
3410     return $date;
3411 }
3412
3413
3414 sub checkin_handle_lost_or_longoverdue {
3415     my ($self, %args) = @_;
3416
3417     my $circ = $self->circ;
3418     my $max_return = $args{max_return};
3419     my $circ_lib = $args{circ_lib};
3420
3421     if ($max_return) {
3422
3423         my $last_activity = 
3424             $self->get_circ_last_billing_activity(
3425                 $circ_lib, $args{ous_use_last_activity});
3426
3427         my $today = time();
3428         my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3429         $tm[5] -= 1 if $tm[5] > 0;
3430         my $due = timelocal(int($tm[1]), int($tm[2]), 
3431             int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3432
3433         my $last_chance = 
3434             OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3435
3436         $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3437             "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3438                 "DUE: $due LAST: $last_chance");
3439
3440         $max_return = 0 if $today < $last_chance;
3441     }
3442
3443
3444     if ($max_return) {
3445
3446         $logger->info("circulator: check-in of lost/lo item exceeds max ". 
3447             "return interval.  skipping fine/fee voiding, etc.");
3448
3449     } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3450
3451         $logger->info("circulator: check-in of lost/lo item having a balance ".
3452             "of zero, skipping fine/fee voiding and reinstatement.");
3453
3454     } else { # within max-return interval or no interval defined
3455
3456         $logger->info("circulator: check-in of lost/lo item is within the ".
3457             "max return interval (or no interval is defined).  Proceeding ".
3458             "with fine/fee voiding, etc.");
3459
3460         $self->needs_lost_bill_handling(1);
3461     }
3462
3463     if ($circ_lib != $self->circ_lib) {
3464         # if the item is not home, check to see if we want to retain the
3465         # lost/longoverdue status at this point in the process
3466
3467         my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, 
3468             $args{ous_immediately_available}, $self->editor) || 0;
3469
3470         if ($immediately_available) {
3471             # item status does not need to be retained, so give it a
3472             # reshelving status as if it were a normal checkin
3473             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3474             $self->update_copy;
3475         } else {
3476             $logger->info("circulator: leaving lost/longoverdue copy".
3477                 " status in place on checkin");
3478         }
3479     } else {
3480         # lost/longoverdue item is home and processed, treat like a normal 
3481         # checkin from this point on
3482         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3483         $self->update_copy;
3484     }
3485 }
3486
3487
3488 sub checkin_handle_backdate {
3489     my $self = shift;
3490
3491     # ------------------------------------------------------------------
3492     # clean up the backdate for date comparison
3493     # XXX We are currently taking the due-time from the original due-date,
3494     # not the input.  Do we need to do this?  This certainly interferes with
3495     # backdating of hourly checkouts, but that is likely a very rare case.
3496     # ------------------------------------------------------------------
3497     my $bd = cleanse_ISO8601($self->backdate);
3498     my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3499     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3500     $new_date->set_hour($original_date->hour());
3501     $new_date->set_minute($original_date->minute());
3502     if ($new_date >= DateTime->now) {
3503         # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3504         # $self->backdate() autoload handler ignores undef values.  
3505         # Clear the backdate manually.
3506         $logger->info("circulator: ignoring future backdate: $new_date");
3507         delete $self->{backdate};
3508     } else {
3509         $self->backdate(cleanse_ISO8601($new_date->datetime()));
3510     }
3511
3512     return undef;
3513 }
3514
3515
3516 sub check_checkin_copy_status {
3517     my $self = shift;
3518    my $copy = $self->copy;
3519
3520    my $status = $U->copy_status($copy->status)->id;
3521
3522    return undef
3523       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
3524             $status == OILS_COPY_STATUS_CHECKED_OUT ||
3525             $status == OILS_COPY_STATUS_IN_PROCESS  ||
3526             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
3527             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
3528             $status == OILS_COPY_STATUS_CATALOGING  ||
3529             $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
3530             $status == OILS_COPY_STATUS_RESHELVING );
3531
3532    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3533       if( $status == OILS_COPY_STATUS_LOST );
3534
3535     return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3536         if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3537
3538    return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3539       if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3540
3541    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3542       if( $status == OILS_COPY_STATUS_MISSING );
3543
3544    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3545 }
3546
3547
3548
3549 # --------------------------------------------------------------------------
3550 # On checkin, we need to return as many relevant objects as we can
3551 # --------------------------------------------------------------------------
3552 sub checkin_flesh_events {
3553     my $self = shift;
3554
3555     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
3556         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3557             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3558     }
3559
3560     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3561
3562     my $hold;
3563     if($self->hold and !$self->hold->cancel_time) {
3564         $hold = $self->hold;
3565         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3566     }
3567
3568     if($self->circ) {
3569         # update our copy of the circ object and 
3570         # flesh the billing summary data
3571         $self->circ(
3572             $self->editor->retrieve_action_circulation([
3573                 $self->circ->id, {
3574                     flesh => 2,
3575                     flesh_fields => {
3576                         circ => ['billable_transaction'],
3577                         mbt => ['summary']
3578                     }
3579                 }
3580             ])
3581         );
3582     }
3583
3584     if($self->patron) {
3585         # flesh some patron fields before returning
3586         $self->patron(
3587             $self->editor->retrieve_actor_user([
3588                 $self->patron->id,
3589                 {
3590                     flesh => 1,
3591                     flesh_fields => {
3592                         au => ['card', 'billing_address', 'mailing_address']
3593                     }
3594                 }
3595             ])
3596         );
3597     }
3598
3599     for my $evt (@{$self->events}) {
3600
3601         my $payload         = {};
3602         $payload->{copy}    = $U->unflesh_copy($self->copy);
3603         $payload->{volume}  = $self->volume;
3604         $payload->{record}  = $record,
3605         $payload->{circ}    = $self->circ;
3606         $payload->{transit} = $self->transit;
3607         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3608         $payload->{hold}    = $hold;
3609         $payload->{patron}  = $self->patron;
3610         $payload->{reservation} = $self->reservation
3611             unless (not $self->reservation or $self->reservation->cancel_time);
3612
3613         $evt->{payload}     = $payload;
3614     }
3615 }
3616
3617 sub log_me {
3618     my( $self, $msg ) = @_;
3619     my $bc = ($self->copy) ? $self->copy->barcode :
3620         $self->barcode;
3621     $bc ||= "";
3622     my $usr = ($self->patron) ? $self->patron->id : "";
3623     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3624         ", recipient=$usr, copy=$bc");
3625 }
3626
3627
3628 sub do_renew {
3629     my $self = shift;
3630     $self->log_me("do_renew()");
3631
3632     # Make sure there is an open circ to renew
3633     my $usrid = $self->patron->id if $self->patron;
3634     my $circ = $self->editor->search_action_circulation({
3635         target_copy => $self->copy->id,
3636         xact_finish => undef,
3637         checkin_time => undef,
3638         ($usrid ? (usr => $usrid) : ())
3639     })->[0];
3640
3641     return $self->bail_on_events($self->editor->event) unless $circ;
3642
3643     # A user is not allowed to renew another user's items without permission
3644     unless( $circ->usr eq $self->editor->requestor->id ) {
3645         return $self->bail_on_events($self->editor->events)
3646             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3647     }   
3648
3649     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3650         if $circ->renewal_remaining < 1;
3651
3652     # -----------------------------------------------------------------
3653
3654     $self->parent_circ($circ->id);
3655     $self->renewal_remaining( $circ->renewal_remaining - 1 );
3656     $self->circ($circ);
3657
3658     # Opac renewal - re-use circ library from original circ (unless told not to)
3659     if($self->opac_renewal) {
3660         unless(defined($opac_renewal_use_circ_lib)) {
3661             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3662             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3663                 $opac_renewal_use_circ_lib = 1;
3664             }
3665             else {
3666                 $opac_renewal_use_circ_lib = 0;
3667             }
3668         }
3669         $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3670     }
3671
3672     # Desk renewal - re-use circ library from original circ (unless told not to)
3673     if($self->desk_renewal) {
3674         unless(defined($desk_renewal_use_circ_lib)) {
3675             my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3676             if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3677                 $desk_renewal_use_circ_lib = 1;
3678             }
3679             else {
3680                 $desk_renewal_use_circ_lib = 0;
3681             }
3682         }
3683         $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3684     }
3685
3686     # Run the fine generator against the old circ
3687     # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3688     # a few lines down.  Commenting out, for now.
3689     #$self->handle_fines;
3690
3691     $self->run_renew_permit;
3692
3693     # Check the item in
3694     $self->do_checkin();
3695     return if $self->bail_out;
3696
3697     unless( $self->permit_override ) {
3698         $self->do_permit();
3699         return if $self->bail_out;
3700         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3701         $self->remove_event('ITEM_NOT_CATALOGED');
3702     }   
3703
3704     $self->override_events;
3705     return if $self->bail_out;
3706
3707     $self->events([]);
3708     $self->do_checkout();
3709 }
3710
3711
3712 sub remove_event {
3713     my( $self, $evt ) = @_;
3714     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3715     $logger->debug("circulator: removing event from list: $evt");
3716     my @events = @{$self->events};
3717     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3718 }
3719
3720
3721 sub have_event {
3722     my( $self, $evt ) = @_;
3723     $evt = (ref $evt) ? $evt->{textcode} : $evt;
3724     return grep { $_->{textcode} eq $evt } @{$self->events};
3725 }
3726
3727
3728 sub run_renew_permit {
3729     my $self = shift;
3730
3731     if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3732         my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3733             $self->editor, $self->copy, $self->editor->requestor, 1
3734         );
3735         $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3736     }
3737
3738     my $results = $self->run_indb_circ_test;
3739     $self->push_events($self->matrix_test_result_events)
3740         unless $self->circ_test_success;
3741 }
3742
3743
3744 # XXX: The primary mechanism for storing circ history is now handled
3745 # by tracking real circulation objects instead of bibs in a bucket.
3746 # However, this code is disabled by default and could be useful 
3747 # some day, so may as well leave it for now.
3748 sub append_reading_list {
3749     my $self = shift;
3750
3751     return undef unless 
3752         $self->is_checkout and 
3753         $self->patron and 
3754         $self->copy and 
3755         !$self->is_noncat;
3756
3757
3758     # verify history is globally enabled and uses the bucket mechanism
3759     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3760         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3761
3762     return undef unless $htype and $htype eq 'bucket';
3763
3764     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3765
3766     # verify the patron wants to retain the hisory
3767     my $setting = $e->search_actor_user_setting(
3768         {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3769     
3770     unless($setting and $setting->value) {
3771         $e->rollback;
3772         return undef;
3773     }
3774
3775     my $bkt = $e->search_container_copy_bucket(
3776         {owner => $self->patron->id, btype => 'circ_history'})->[0];
3777
3778     my $pos = 1;
3779
3780     if($bkt) {
3781         # find the next item position
3782         my $last_item = $e->search_container_copy_bucket_item(
3783             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3784         $pos = $last_item->pos + 1 if $last_item;
3785
3786     } else {
3787         # create the history bucket if necessary
3788         $bkt = Fieldmapper::container::copy_bucket->new;
3789         $bkt->owner($self->patron->id);
3790         $bkt->name('');
3791         $bkt->btype('circ_history');
3792         $bkt->pub('f');
3793         $e->create_container_copy_bucket($bkt) or return $e->die_event;
3794     }
3795
3796     my $item = Fieldmapper::container::copy_bucket_item->new;
3797
3798     $item->bucket($bkt->id);
3799     $item->target_copy($self->copy->id);
3800     $item->pos($pos);
3801
3802     $e->create_container_copy_bucket_item($item) or return $e->die_event;
3803     $e->commit;
3804
3805     return undef;
3806 }
3807
3808
3809 sub make_trigger_events {
3810     my $self = shift;
3811     return unless $self->circ;
3812     $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3813     $U->create_events_for_hook('checkin',  $self->circ, $self->circ_lib) if $self->is_checkin;
3814     $U->create_events_for_hook('renewal',  $self->circ, $self->circ_lib) if $self->is_renewal;
3815 }
3816
3817
3818
3819 sub checkin_handle_lost_or_lo_now_found {
3820     my ($self, $bill_type, $is_longoverdue) = @_;
3821
3822     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3823
3824     $logger->debug("voiding $tag item billings");
3825     my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
3826     $self->bail_on_events($self->editor->event) if ($result);
3827 }
3828
3829 sub checkin_handle_lost_or_lo_now_found_restore_od {
3830     my $self = shift;
3831     my $circ_lib = shift;
3832     my $is_longoverdue = shift;
3833     my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3834
3835     # ------------------------------------------------------------------
3836     # restore those overdue charges voided when item was set to lost
3837     # ------------------------------------------------------------------
3838
3839     my $ods = $self->editor->search_money_billing([
3840         {
3841             xact => $self->circ->id,
3842             btype => 1
3843         },
3844         {
3845             order_by => {mb => 'billing_ts desc'}
3846         }
3847     ]);
3848
3849     $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
3850     # Because actual users get up to all kinds of unexpectedness, we
3851     # only recreate up to $circ->max_fine in bills.  I know you think
3852     # it wouldn't happen that bills could get created, voided, and
3853     # recreated more than once, but I guaran-damn-tee you that it will
3854     # happen.
3855     if ($ods && @$ods) {
3856         my $void_amount = 0;
3857         my $void_max = $self->circ->max_fine();
3858         # search for overdues voided the new way (aka "adjusted")
3859         my @billings = map {$_->id()} @$ods;
3860         my $voids = $self->editor->search_money_account_adjustment(
3861             {
3862                 billing => \@billings
3863             }
3864         );
3865         if (@$voids) {
3866             map {$void_amount += $_->amount()} @$voids;
3867         } else {
3868             # if no adjustments found, assume they were voided the old way (aka "voided")
3869             for my $bill (@$ods) {
3870                 if( $U->is_true($bill->voided) ) {
3871                     $void_amount += $bill->amount();
3872                 }
3873             }
3874         }
3875         $CC->create_bill(
3876             $self->editor,
3877             ($void_amount < $void_max ? $void_amount : $void_max),
3878             $ods->[0]->btype(),
3879             $ods->[0]->billing_type(),
3880             $self->circ->id(),
3881             "System: $tag RETURNED - OVERDUES REINSTATED",
3882             $ods->[0]->billing_ts() # date this restoration the same as the last overdue (for possible subsequent fine generation)
3883         );
3884     }
3885 }
3886
3887 1;