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