1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
11 my $U = "OpenILS::Application::AppUtils";
15 my $legacy_script_support = 0;
17 my $opac_renewal_use_circ_lib;
19 sub determine_booking_status {
20 unless (defined $booking_status) {
21 my $ses = create OpenSRF::AppSession("router");
22 $booking_status = grep {$_ eq "open-ils.booking"} @{
23 $ses->request("opensrf.router.info.class.list")->gather(1)
26 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
29 return $booking_status;
35 flesh_fields => {acp => ['call_number','parts'], acn => ['record']}
41 my $conf = OpenSRF::Utils::SettingsClient->new;
42 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
44 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
45 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
47 my $lb = $conf->config_value( @pfx2, 'script_path' );
48 $lb = [ $lb ] unless ref($lb);
51 return unless $legacy_script_support;
53 my @pfx = ( @pfx2, "scripts" );
54 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
55 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
56 my $d = $conf->config_value( @pfx, 'circ_duration' );
57 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
58 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
59 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
61 $logger->error( "Missing circ script(s)" )
62 unless( $p and $c and $d and $f and $m and $pr );
64 $scripts{circ_permit_patron} = $p;
65 $scripts{circ_permit_copy} = $c;
66 $scripts{circ_duration} = $d;
67 $scripts{circ_recurring_fines} = $f;
68 $scripts{circ_max_fines} = $m;
69 $scripts{circ_permit_renew} = $pr;
72 "circulator: Loaded rules scripts for circ: " .
73 "circ permit patron = $p, ".
74 "circ permit copy = $c, ".
75 "circ duration = $d, ".
76 "circ recurring fines = $f, " .
77 "circ max fines = $m, ".
78 "circ renew permit = $pr. ".
80 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
84 __PACKAGE__->register_method(
85 method => "run_method",
86 api_name => "open-ils.circ.checkout.permit",
88 Determines if the given checkout can occur
89 @param authtoken The login session key
90 @param params A trailing hash of named params including
91 barcode : The copy barcode,
92 patron : The patron the checkout is occurring for,
93 renew : true or false - whether or not this is a renewal
94 @return The event that occurred during the permit check.
98 __PACKAGE__->register_method (
99 method => 'run_method',
100 api_name => 'open-ils.circ.checkout.permit.override',
101 signature => q/@see open-ils.circ.checkout.permit/,
105 __PACKAGE__->register_method(
106 method => "run_method",
107 api_name => "open-ils.circ.checkout",
110 @param authtoken The login session key
111 @param params A named hash of params including:
113 barcode If no copy is provided, the copy is retrieved via barcode
114 copyid If no copy or barcode is provide, the copy id will be use
115 patron The patron's id
116 noncat True if this is a circulation for a non-cataloted item
117 noncat_type The non-cataloged type id
118 noncat_circ_lib The location for the noncat circ.
119 precat The item has yet to be cataloged
120 dummy_title The temporary title of the pre-cataloded item
121 dummy_author The temporary authr of the pre-cataloded item
122 Default is the home org of the staff member
123 @return The SUCCESS event on success, any other event depending on the error
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.checkin",
131 Generic super-method for handling all copies
132 @param authtoken The login session key
133 @param params Hash of named parameters including:
134 barcode - The copy barcode
135 force - If true, copies in bad statuses will be checked in and give good statuses
136 noop - don't capture holds or put items into transit
137 void_overdues - void all overdues for the circulation (aka amnesty)
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkin.override",
145 signature => q/@see open-ils.circ.checkin/
148 __PACKAGE__->register_method(
149 method => "run_method",
150 api_name => "open-ils.circ.renew.override",
151 signature => q/@see open-ils.circ.renew/,
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.renew",
158 notes => <<" NOTES");
159 PARAMS( authtoken, circ => circ_id );
160 open-ils.circ.renew(login_session, circ_object);
161 Renews the provided circulation. login_session is the requestor of the
162 renewal and if the logged in user is not the same as circ->usr, then
163 the logged in user must have RENEW_CIRC permissions.
166 __PACKAGE__->register_method(
167 method => "run_method",
168 api_name => "open-ils.circ.checkout.full"
170 __PACKAGE__->register_method(
171 method => "run_method",
172 api_name => "open-ils.circ.checkout.full.override"
174 __PACKAGE__->register_method(
175 method => "run_method",
176 api_name => "open-ils.circ.reservation.pickup"
178 __PACKAGE__->register_method(
179 method => "run_method",
180 api_name => "open-ils.circ.reservation.return"
182 __PACKAGE__->register_method(
183 method => "run_method",
184 api_name => "open-ils.circ.reservation.return.override"
186 __PACKAGE__->register_method(
187 method => "run_method",
188 api_name => "open-ils.circ.checkout.inspect",
189 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
194 my( $self, $conn, $auth, $args, $oargs ) = @_;
195 translate_legacy_args($args);
196 $oargs = { all => 1 } unless defined $oargs;
197 my $api = $self->api_name;
200 OpenILS::Application::Circ::Circulator->new($auth, %$args, $oargs);
202 return circ_events($circulator) if $circulator->bail_out;
204 $circulator->use_booking(determine_booking_status());
206 # --------------------------------------------------------------------------
207 # First, check for a booking transit, as the barcode may not be a copy
208 # barcode, but a resource barcode, and nothing else in here will work
209 # --------------------------------------------------------------------------
211 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
212 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
213 if (@$resources) { # yes!
215 my $res_id_list = [ map { $_->id } @$resources ];
216 my $transit = $circulator->editor->search_action_reservation_transit_copy(
218 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
219 { order_by => { artc => 'source_send_time' }, limit => 1 }
221 )->[0]; # Any transit for this barcode?
223 if ($transit) { # yes! unwrap it.
225 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
226 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
228 my $success_event = new OpenILS::Event(
229 "SUCCESS", "payload" => {"reservation" => $reservation}
231 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
232 if (my $copy = $circulator->editor->search_asset_copy([
233 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
234 ])->[0]) { # got a copy
235 $copy->status( $transit->copy_status );
236 $copy->editor($circulator->editor->requestor->id);
237 $copy->edit_date('now');
238 $circulator->editor->update_asset_copy($copy);
239 $success_event->{"payload"}->{"record"} =
240 $U->record_to_mvr($copy->call_number->record);
241 $success_event->{"payload"}->{"volume"} = $copy->call_number;
242 $copy->call_number($copy->call_number->id);
243 $success_event->{"payload"}->{"copy"} = $copy;
247 $transit->dest_recv_time('now');
248 $circulator->editor->update_action_reservation_transit_copy( $transit );
250 $circulator->editor->commit;
251 # Formerly this branch just stopped here. Argh!
252 $conn->respond_complete($success_event);
260 # --------------------------------------------------------------------------
261 # Go ahead and load the script runner to make sure we have all
262 # of the objects we need
263 # --------------------------------------------------------------------------
265 if ($circulator->use_booking) {
266 $circulator->is_res_checkin($circulator->is_checkin(1))
267 if $api =~ /reservation.return/ or (
268 $api =~ /checkin/ and $circulator->seems_like_reservation()
271 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
274 $circulator->is_renewal(1) if $api =~ /renew/;
275 $circulator->is_checkin(1) if $api =~ /checkin/;
277 $circulator->mk_env();
278 $circulator->noop(1) if $circulator->claims_never_checked_out;
280 if($legacy_script_support and not $circulator->is_checkin) {
281 $circulator->mk_script_runner();
282 $circulator->legacy_script_support(1);
283 $circulator->circ_permit_patron($scripts{circ_permit_patron});
284 $circulator->circ_permit_copy($scripts{circ_permit_copy});
285 $circulator->circ_duration($scripts{circ_duration});
286 $circulator->circ_permit_renew($scripts{circ_permit_renew});
288 return circ_events($circulator) if $circulator->bail_out;
291 $circulator->override(1) if $api =~ /override/o;
293 if( $api =~ /checkout\.permit/ ) {
294 $circulator->do_permit();
296 } elsif( $api =~ /checkout.full/ ) {
298 # requesting a precat checkout implies that any required
299 # overrides have been performed. Go ahead and re-override.
300 $circulator->skip_permit_key(1);
301 $circulator->override(1) if $circulator->request_precat;
302 $circulator->do_permit();
303 $circulator->is_checkout(1);
304 unless( $circulator->bail_out ) {
305 $circulator->events([]);
306 $circulator->do_checkout();
309 } elsif( $circulator->is_res_checkout ) {
310 $circulator->do_reservation_pickup();
312 } elsif( $api =~ /inspect/ ) {
313 my $data = $circulator->do_inspect();
314 $circulator->editor->rollback;
317 } elsif( $api =~ /checkout/ ) {
318 $circulator->is_checkout(1);
319 $circulator->do_checkout();
321 } elsif( $circulator->is_res_checkin ) {
322 $circulator->do_reservation_return();
323 $circulator->do_checkin() if ($circulator->copy());
324 } elsif( $api =~ /checkin/ ) {
325 $circulator->do_checkin();
327 } elsif( $api =~ /renew/ ) {
328 $circulator->is_renewal(1);
329 $circulator->do_renew();
332 if( $circulator->bail_out ) {
335 # make sure no success event accidentally slip in
337 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
340 my @e = @{$circulator->events};
341 push( @ee, $_->{textcode} ) for @e;
342 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
344 $circulator->editor->rollback;
348 $circulator->editor->commit;
350 if ($circulator->generate_lost_overdue) {
351 # Generating additional overdue billings has to happen after the
352 # main commit and before the final respond() so the caller can
353 # receive the latest transaction summary.
354 my $evt = $circulator->generate_lost_overdue_fines;
355 $circulator->bail_on_events($evt) if $evt;
359 $conn->respond_complete(circ_events($circulator));
361 $circulator->script_runner->cleanup if $circulator->script_runner;
363 return undef if $circulator->bail_out;
365 $circulator->do_hold_notify($circulator->notify_hold)
366 if $circulator->notify_hold;
367 $circulator->retarget_holds if $circulator->retarget;
368 $circulator->append_reading_list;
369 $circulator->make_trigger_events;
376 my @e = @{$circ->events};
377 # if we have multiple events, SUCCESS should not be one of them;
378 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
379 return (@e == 1) ? $e[0] : \@e;
383 sub translate_legacy_args {
386 if( $$args{barcode} ) {
387 $$args{copy_barcode} = $$args{barcode};
388 delete $$args{barcode};
391 if( $$args{copyid} ) {
392 $$args{copy_id} = $$args{copyid};
393 delete $$args{copyid};
396 if( $$args{patronid} ) {
397 $$args{patron_id} = $$args{patronid};
398 delete $$args{patronid};
401 if( $$args{patron} and !ref($$args{patron}) ) {
402 $$args{patron_id} = $$args{patron};
403 delete $$args{patron};
407 if( $$args{noncat} ) {
408 $$args{is_noncat} = $$args{noncat};
409 delete $$args{noncat};
412 if( $$args{precat} ) {
413 $$args{is_precat} = $$args{request_precat} = $$args{precat};
414 delete $$args{precat};
420 # --------------------------------------------------------------------------
421 # This package actually manages all of the circulation logic
422 # --------------------------------------------------------------------------
423 package OpenILS::Application::Circ::Circulator;
424 use strict; use warnings;
425 use vars q/$AUTOLOAD/;
427 use OpenILS::Utils::Fieldmapper;
428 use OpenSRF::Utils::Cache;
429 use Digest::MD5 qw(md5_hex);
430 use DateTime::Format::ISO8601;
431 use OpenILS::Utils::PermitHold;
432 use OpenSRF::Utils qw/:datetime/;
433 use OpenSRF::Utils::SettingsClient;
434 use OpenILS::Application::Circ::Holds;
435 use OpenILS::Application::Circ::Transit;
436 use OpenSRF::Utils::Logger qw(:logger);
437 use OpenILS::Utils::CStoreEditor qw/:funcs/;
438 use OpenILS::Application::Circ::ScriptBuilder;
439 use OpenILS::Const qw/:const/;
440 use OpenILS::Utils::Penalty;
441 use OpenILS::Application::Circ::CircCommon;
444 my $holdcode = "OpenILS::Application::Circ::Holds";
445 my $transcode = "OpenILS::Application::Circ::Transit";
451 # --------------------------------------------------------------------------
452 # Add a pile of automagic getter/setter methods
453 # --------------------------------------------------------------------------
454 my @AUTOLOAD_FIELDS = qw/
501 recurring_fines_level
514 cancelled_hold_transit
521 circ_matrix_matchpoint
523 legacy_script_support
533 claims_never_checked_out
538 generate_lost_overdue
550 my $type = ref($self) or die "$self is not an object";
552 my $name = $AUTOLOAD;
555 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
556 $logger->error("circulator: $type: invalid autoload field: $name");
557 die "$type: invalid autoload field: $name\n"
562 *{"${type}::${name}"} = sub {
565 $s->{$name} = $v if defined $v;
569 return $self->$name($data);
574 my( $class, $auth, %args, $oargs ) = @_;
575 $class = ref($class) || $class;
576 my $self = bless( {}, $class );
579 $self->editor(new_editor(xact => 1, authtoken => $auth));
580 $self->override_args($oargs);
582 unless( $self->editor->checkauth ) {
583 $self->bail_on_events($self->editor->event);
587 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
589 $self->$_($args{$_}) for keys %args;
592 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
594 # if this is a renewal, default to desk_renewal
595 $self->desk_renewal(1) unless
596 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
598 $self->capture('') unless $self->capture;
600 unless(%user_groups) {
601 my $gps = $self->editor->retrieve_all_permission_grp_tree;
602 %user_groups = map { $_->id => $_ } @$gps;
609 # --------------------------------------------------------------------------
610 # True if we should discontinue processing
611 # --------------------------------------------------------------------------
613 my( $self, $bool ) = @_;
614 if( defined $bool ) {
615 $logger->info("circulator: BAILING OUT") if $bool;
616 $self->{bail_out} = $bool;
618 return $self->{bail_out};
623 my( $self, @evts ) = @_;
626 $e->{payload} = $self->copy if
627 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
629 $logger->info("circulator: pushing event ".$e->{textcode});
630 push( @{$self->events}, $e ) unless
631 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
637 return '' if $self->skip_permit_key;
638 my $key = md5_hex( time() . rand() . "$$" );
639 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
640 return $self->permit_key($key);
643 sub check_permit_key {
645 return 1 if $self->skip_permit_key;
646 my $key = $self->permit_key;
647 return 0 unless $key;
648 my $k = "oils_permit_key_$key";
649 my $one = $self->cache_handle->get_cache($k);
650 $self->cache_handle->delete_cache($k);
651 return ($one) ? 1 : 0;
654 sub seems_like_reservation {
657 # Some words about the following method:
658 # 1) It requires the VIEW_USER permission, but that's not an
659 # issue, right, since all staff should have that?
660 # 2) It returns only one reservation at a time, even if an item can be
661 # and is currently overbooked. Hmmm....
662 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
663 my $result = $booking_ses->request(
664 "open-ils.booking.reservations.by_returnable_resource_barcode",
665 $self->editor->authtoken,
668 $booking_ses->disconnect;
670 return $self->bail_on_events($result) if defined $U->event_code($result);
673 $self->reservation(shift @$result);
681 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
682 sub save_trimmed_copy {
683 my ($self, $copy) = @_;
686 $self->volume($copy->call_number);
687 $self->title($self->volume->record);
688 $self->copy->call_number($self->volume->id);
689 $self->volume->record($self->title->id);
690 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
691 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
692 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
693 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
699 my $e = $self->editor;
701 # --------------------------------------------------------------------------
702 # Grab the fleshed copy
703 # --------------------------------------------------------------------------
704 unless($self->is_noncat) {
707 $copy = $e->retrieve_asset_copy(
708 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
710 } elsif( $self->copy_barcode ) {
712 $copy = $e->search_asset_copy(
713 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
714 } elsif( $self->reservation ) {
715 my $res = $e->json_query(
717 "select" => {"acp" => ["id"]},
722 "field" => "barcode",
726 "field" => "current_resource"
734 "id" => (ref $self->reservation) ?
735 $self->reservation->id : $self->reservation
740 if (ref $res eq "ARRAY" and scalar @$res) {
741 $logger->info("circulator: mapped reservation " .
742 $self->reservation . " to copy " . $res->[0]->{"id"});
743 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
748 $self->save_trimmed_copy($copy);
750 # We can't renew if there is no copy
751 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
752 if $self->is_renewal;
757 # --------------------------------------------------------------------------
759 # --------------------------------------------------------------------------
763 flesh_fields => {au => [ qw/ card / ]}
766 if( $self->patron_id ) {
767 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
768 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
770 } elsif( $self->patron_barcode ) {
772 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
773 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
774 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
776 $patron = $e->retrieve_actor_user($card->usr)
777 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
779 # Use the card we looked up, not the patron's primary, for card active checks
780 $patron->card($card);
783 if( my $copy = $self->copy ) {
786 $flesh->{flesh_fields}->{circ} = ['usr'];
788 my $circ = $e->search_action_circulation([
789 {target_copy => $copy->id, checkin_time => undef}, $flesh
793 $patron = $circ->usr;
794 $circ->usr($patron->id); # de-flesh for consistency
800 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
801 unless $self->patron($patron) or $self->is_checkin;
803 unless($self->is_checkin) {
805 # Check for inactivity and patron reg. expiration
807 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
808 unless $U->is_true($patron->active);
810 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
811 unless $U->is_true($patron->card->active);
813 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
814 cleanse_ISO8601($patron->expire_date));
816 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
817 if( CORE::time > $expire->epoch ) ;
821 # --------------------------------------------------------------------------
822 # This builds the script runner environment and fetches most of the
824 # --------------------------------------------------------------------------
825 sub mk_script_runner {
831 qw/copy copy_barcode copy_id patron
832 patron_id patron_barcode volume title editor/;
834 # Translate our objects into the ScriptBuilder args hash
835 $$args{$_} = $self->$_() for @fields;
837 $args->{ignore_user_status} = 1 if $self->is_checkin;
838 $$args{fetch_patron_by_circ_copy} = 1;
839 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
841 if( my $pco = $self->pending_checkouts ) {
842 $logger->info("circulator: we were given a pending checkouts number of $pco");
843 $$args{patronItemsOut} = $pco;
846 # This fetches most of the objects we need
847 $self->script_runner(
848 OpenILS::Application::Circ::ScriptBuilder->build($args));
850 # Now we translate the ScriptBuilder objects back into self
851 $self->$_($$args{$_}) for @fields;
853 my @evts = @{$args->{_events}} if $args->{_events};
855 $logger->debug("circulator: script builder returned events: @evts") if @evts;
859 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
860 if(!$self->is_noncat and
862 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
866 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
867 return $self->bail_on_events(@e);
872 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
873 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
874 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
875 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
879 # We can't renew if there is no copy
880 return $self->bail_on_events(@evts) if
881 $self->is_renewal and !$self->copy;
883 # Set some circ-specific flags in the script environment
884 my $evt = "environment";
885 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
887 if( $self->is_noncat ) {
888 $self->script_runner->insert("$evt.isNonCat", 1);
889 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
892 if( $self->is_precat ) {
893 $self->script_runner->insert("environment.isPrecat", 1, 1);
896 $self->script_runner->add_path( $_ ) for @$script_libs;
901 # --------------------------------------------------------------------------
902 # Does the circ permit work
903 # --------------------------------------------------------------------------
907 $self->log_me("do_permit()");
909 unless( $self->editor->requestor->id == $self->patron->id ) {
910 return $self->bail_on_events($self->editor->event)
911 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
914 $self->check_captured_holds();
915 $self->do_copy_checks();
916 return if $self->bail_out;
917 $self->run_patron_permit_scripts();
918 $self->run_copy_permit_scripts()
919 unless $self->is_precat or $self->is_noncat;
920 $self->check_item_deposit_events();
921 $self->override_events();
922 return if $self->bail_out;
924 if($self->is_precat and not $self->request_precat) {
927 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
928 return $self->bail_out(1) unless $self->is_renewal;
932 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
935 sub check_item_deposit_events {
937 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
938 if $self->is_deposit and not $self->is_deposit_exempt;
939 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
940 if $self->is_rental and not $self->is_rental_exempt;
943 # returns true if the user is not required to pay deposits
944 sub is_deposit_exempt {
946 my $pid = (ref $self->patron->profile) ?
947 $self->patron->profile->id : $self->patron->profile;
948 my $groups = $U->ou_ancestor_setting_value(
949 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
950 for my $grp (@$groups) {
951 return 1 if $self->is_group_descendant($grp, $pid);
956 # returns true if the user is not required to pay rental fees
957 sub is_rental_exempt {
959 my $pid = (ref $self->patron->profile) ?
960 $self->patron->profile->id : $self->patron->profile;
961 my $groups = $U->ou_ancestor_setting_value(
962 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
963 for my $grp (@$groups) {
964 return 1 if $self->is_group_descendant($grp, $pid);
969 sub is_group_descendant {
970 my($self, $p_id, $c_id) = @_;
971 return 0 unless defined $p_id and defined $c_id;
972 return 1 if $c_id == $p_id;
973 while(my $grp = $user_groups{$c_id}) {
974 $c_id = $grp->parent;
975 return 0 unless defined $c_id;
976 return 1 if $c_id == $p_id;
981 sub check_captured_holds {
983 my $copy = $self->copy;
984 my $patron = $self->patron;
986 return undef unless $copy;
988 my $s = $U->copy_status($copy->status)->id;
989 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
990 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
992 # Item is on the holds shelf, make sure it's going to the right person
993 my $holds = $self->editor->search_action_hold_request(
996 current_copy => $copy->id ,
997 capture_time => { '!=' => undef },
998 cancel_time => undef,
999 fulfillment_time => undef
1005 if( $holds and $$holds[0] ) {
1006 return undef if $$holds[0]->usr == $patron->id;
1009 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1011 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1015 sub do_copy_checks {
1017 my $copy = $self->copy;
1018 return unless $copy;
1020 my $stat = $U->copy_status($copy->status)->id;
1022 # We cannot check out a copy if it is in-transit
1023 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1024 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1027 $self->handle_claims_returned();
1028 return if $self->bail_out;
1030 # no claims returned circ was found, check if there is any open circ
1031 unless( $self->is_renewal ) {
1033 my $circs = $self->editor->search_action_circulation(
1034 { target_copy => $copy->id, checkin_time => undef }
1037 if(my $old_circ = $circs->[0]) { # an open circ was found
1039 my $payload = {copy => $copy};
1041 if($old_circ->usr == $self->patron->id) {
1043 $payload->{old_circ} = $old_circ;
1045 # If there is an open circulation on the checkout item and an auto-renew
1046 # interval is defined, inform the caller that they should go
1047 # ahead and renew the item instead of warning about open circulations.
1049 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1051 'circ.checkout_auto_renew_age',
1055 if($auto_renew_intvl) {
1056 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1057 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1059 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1060 $payload->{auto_renew} = 1;
1065 return $self->bail_on_events(
1066 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1072 my $LEGACY_CIRC_EVENT_MAP = {
1073 'no_item' => 'ITEM_NOT_CATALOGED',
1074 'actor.usr.barred' => 'PATRON_BARRED',
1075 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1076 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1077 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1078 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1079 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1080 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1081 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1082 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1086 # ---------------------------------------------------------------------
1087 # This pushes any patron-related events into the list but does not
1088 # set bail_out for any events
1089 # ---------------------------------------------------------------------
1090 sub run_patron_permit_scripts {
1092 my $runner = $self->script_runner;
1093 my $patronid = $self->patron->id;
1097 if(!$self->legacy_script_support) {
1099 my $results = $self->run_indb_circ_test;
1100 unless($self->circ_test_success) {
1101 # no_item result is OK during noncat checkout
1102 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1103 push @allevents, $self->matrix_test_result_events;
1109 # ---------------------------------------------------------------------
1110 # # Now run the patron permit script
1111 # ---------------------------------------------------------------------
1112 $runner->load($self->circ_permit_patron);
1113 my $result = $runner->run or
1114 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1116 my $patron_events = $result->{events};
1118 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1119 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1120 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1121 $penalties = $penalties->{fatal_penalties};
1123 for my $pen (@$penalties) {
1124 my $event = OpenILS::Event->new($pen->name);
1125 $event->{desc} = $pen->label;
1126 push(@allevents, $event);
1129 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1133 $_->{payload} = $self->copy if
1134 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1137 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1139 $self->push_events(@allevents);
1142 sub matrix_test_result_codes {
1144 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1147 sub matrix_test_result_events {
1150 my $event = new OpenILS::Event(
1151 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1153 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1155 } (@{$self->matrix_test_result});
1158 sub run_indb_circ_test {
1160 return $self->matrix_test_result if $self->matrix_test_result;
1162 my $dbfunc = ($self->is_renewal) ?
1163 'action.item_user_renew_test' : 'action.item_user_circ_test';
1165 if( $self->is_precat && $self->request_precat) {
1166 $self->make_precat_copy;
1167 return if $self->bail_out;
1170 my $results = $self->editor->json_query(
1174 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1180 $self->circ_test_success($U->is_true($results->[0]->{success}));
1182 if(my $mp = $results->[0]->{matchpoint}) {
1183 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1184 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1185 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1186 if(defined($results->[0]->{renewals})) {
1187 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1189 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1190 if(defined($results->[0]->{grace_period})) {
1191 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1193 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1194 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1195 # Grab the *last* response for limit_groups, where it is more likely to be filled
1196 $self->limit_groups($results->[-1]->{limit_groups});
1199 return $self->matrix_test_result($results);
1202 # ---------------------------------------------------------------------
1203 # given a use and copy, this will calculate the circulation policy
1204 # parameters. Only works with in-db circ.
1205 # ---------------------------------------------------------------------
1209 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1211 $self->run_indb_circ_test;
1214 circ_test_success => $self->circ_test_success,
1215 failure_events => [],
1216 failure_codes => [],
1217 matchpoint => $self->circ_matrix_matchpoint
1220 unless($self->circ_test_success) {
1221 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1222 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1225 if($self->circ_matrix_matchpoint) {
1226 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1227 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1228 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1229 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1231 my $policy = $self->get_circ_policy(
1232 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1234 $$results{$_} = $$policy{$_} for keys %$policy;
1240 # ---------------------------------------------------------------------
1241 # Loads the circ policy info for duration, recurring fine, and max
1242 # fine based on the current copy
1243 # ---------------------------------------------------------------------
1244 sub get_circ_policy {
1245 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1248 duration_rule => $duration_rule->name,
1249 recurring_fine_rule => $recurring_fine_rule->name,
1250 max_fine_rule => $max_fine_rule->name,
1251 max_fine => $self->get_max_fine_amount($max_fine_rule),
1252 fine_interval => $recurring_fine_rule->recurrence_interval,
1253 renewal_remaining => $duration_rule->max_renewals,
1254 grace_period => $recurring_fine_rule->grace_period
1257 if($hard_due_date) {
1258 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1259 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1262 $policy->{duration_date_ceiling} = undef;
1263 $policy->{duration_date_ceiling_force} = undef;
1266 $policy->{duration} = $duration_rule->shrt
1267 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1268 $policy->{duration} = $duration_rule->normal
1269 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1270 $policy->{duration} = $duration_rule->extended
1271 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1273 $policy->{recurring_fine} = $recurring_fine_rule->low
1274 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1275 $policy->{recurring_fine} = $recurring_fine_rule->normal
1276 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1277 $policy->{recurring_fine} = $recurring_fine_rule->high
1278 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1283 sub get_max_fine_amount {
1285 my $max_fine_rule = shift;
1286 my $max_amount = $max_fine_rule->amount;
1288 # if is_percent is true then the max->amount is
1289 # use as a percentage of the copy price
1290 if ($U->is_true($max_fine_rule->is_percent)) {
1291 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1292 $max_amount = $price * $max_fine_rule->amount / 100;
1294 $U->ou_ancestor_setting_value(
1296 'circ.max_fine.cap_at_price',
1300 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1301 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1309 sub run_copy_permit_scripts {
1311 my $copy = $self->copy || return;
1312 my $runner = $self->script_runner;
1316 if(!$self->legacy_script_support) {
1317 my $results = $self->run_indb_circ_test;
1318 push @allevents, $self->matrix_test_result_events
1319 unless $self->circ_test_success;
1322 # ---------------------------------------------------------------------
1323 # Capture all of the copy permit events
1324 # ---------------------------------------------------------------------
1325 $runner->load($self->circ_permit_copy);
1326 my $result = $runner->run or
1327 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1328 my $copy_events = $result->{events};
1330 # ---------------------------------------------------------------------
1331 # Now collect all of the events together
1332 # ---------------------------------------------------------------------
1333 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1336 # See if this copy has an alert message
1337 my $ae = $self->check_copy_alert();
1338 push( @allevents, $ae ) if $ae;
1340 # uniquify the events
1341 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1342 @allevents = values %hash;
1344 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1346 $self->push_events(@allevents);
1350 sub check_copy_alert {
1352 return undef if $self->is_renewal;
1353 return OpenILS::Event->new(
1354 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1355 if $self->copy and $self->copy->alert_message;
1361 # --------------------------------------------------------------------------
1362 # If the call is overriding and has permissions to override every collected
1363 # event, the are cleared. Any event that the caller does not have
1364 # permission to override, will be left in the event list and bail_out will
1366 # XXX We need code in here to cancel any holds/transits on copies
1367 # that are being force-checked out
1368 # --------------------------------------------------------------------------
1369 sub override_events {
1371 my @events = @{$self->events};
1372 return unless @events;
1373 my $oargs = $self->override_args;
1375 if(!$self->override) {
1376 return $self->bail_out(1)
1377 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1382 for my $e (@events) {
1383 my $tc = $e->{textcode};
1384 next if $tc eq 'SUCCESS';
1385 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1386 my $ov = "$tc.override";
1387 $logger->info("circulator: attempting to override event: $ov");
1389 return $self->bail_on_events($self->editor->event)
1390 unless( $self->editor->allowed($ov) );
1392 return $self->bail_out(1);
1398 # --------------------------------------------------------------------------
1399 # If there is an open claimsreturn circ on the requested copy, close the
1400 # circ if overriding, otherwise bail out
1401 # --------------------------------------------------------------------------
1402 sub handle_claims_returned {
1404 my $copy = $self->copy;
1406 my $CR = $self->editor->search_action_circulation(
1408 target_copy => $copy->id,
1409 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1410 checkin_time => undef,
1414 return unless ($CR = $CR->[0]);
1418 # - If the caller has set the override flag, we will check the item in
1419 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1421 $CR->checkin_time('now');
1422 $CR->checkin_scan_time('now');
1423 $CR->checkin_lib($self->circ_lib);
1424 $CR->checkin_workstation($self->editor->requestor->wsid);
1425 $CR->checkin_staff($self->editor->requestor->id);
1427 $evt = $self->editor->event
1428 unless $self->editor->update_action_circulation($CR);
1431 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1434 $self->bail_on_events($evt) if $evt;
1439 # --------------------------------------------------------------------------
1440 # This performs the checkout
1441 # --------------------------------------------------------------------------
1445 $self->log_me("do_checkout()");
1447 # make sure perms are good if this isn't a renewal
1448 unless( $self->is_renewal ) {
1449 return $self->bail_on_events($self->editor->event)
1450 unless( $self->editor->allowed('COPY_CHECKOUT') );
1453 # verify the permit key
1454 unless( $self->check_permit_key ) {
1455 if( $self->permit_override ) {
1456 return $self->bail_on_events($self->editor->event)
1457 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1459 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1463 # if this is a non-cataloged circ, build the circ and finish
1464 if( $self->is_noncat ) {
1465 $self->checkout_noncat;
1467 OpenILS::Event->new('SUCCESS',
1468 payload => { noncat_circ => $self->circ }));
1472 if( $self->is_precat ) {
1473 $self->make_precat_copy;
1474 return if $self->bail_out;
1476 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1477 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1480 $self->do_copy_checks;
1481 return if $self->bail_out;
1483 $self->run_checkout_scripts();
1484 return if $self->bail_out;
1486 $self->build_checkout_circ_object();
1487 return if $self->bail_out;
1489 my $modify_to_start = $self->booking_adjusted_due_date();
1490 return if $self->bail_out;
1492 $self->apply_modified_due_date($modify_to_start);
1493 return if $self->bail_out;
1495 return $self->bail_on_events($self->editor->event)
1496 unless $self->editor->create_action_circulation($self->circ);
1498 # refresh the circ to force local time zone for now
1499 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1501 if($self->limit_groups) {
1502 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1505 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1507 return if $self->bail_out;
1509 $self->apply_deposit_fee();
1510 return if $self->bail_out;
1512 $self->handle_checkout_holds();
1513 return if $self->bail_out;
1515 # ------------------------------------------------------------------------------
1516 # Update the patron penalty info in the DB. Run it for permit-overrides
1517 # since the penalties are not updated during the permit phase
1518 # ------------------------------------------------------------------------------
1519 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1521 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1524 if($self->is_renewal) {
1525 # flesh the billing summary for the checked-in circ
1526 $pcirc = $self->editor->retrieve_action_circulation([
1528 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1533 OpenILS::Event->new('SUCCESS',
1535 copy => $U->unflesh_copy($self->copy),
1536 volume => $self->volume,
1537 circ => $self->circ,
1539 holds_fulfilled => $self->fulfilled_holds,
1540 deposit_billing => $self->deposit_billing,
1541 rental_billing => $self->rental_billing,
1542 parent_circ => $pcirc,
1543 patron => ($self->return_patron) ? $self->patron : undef,
1544 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1550 sub apply_deposit_fee {
1552 my $copy = $self->copy;
1554 ($self->is_deposit and not $self->is_deposit_exempt) or
1555 ($self->is_rental and not $self->is_rental_exempt);
1557 return if $self->is_deposit and $self->skip_deposit_fee;
1558 return if $self->is_rental and $self->skip_rental_fee;
1560 my $bill = Fieldmapper::money::billing->new;
1561 my $amount = $copy->deposit_amount;
1565 if($self->is_deposit) {
1566 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1568 $self->deposit_billing($bill);
1570 $billing_type = OILS_BILLING_TYPE_RENTAL;
1572 $self->rental_billing($bill);
1575 $bill->xact($self->circ->id);
1576 $bill->amount($amount);
1577 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1578 $bill->billing_type($billing_type);
1579 $bill->btype($btype);
1580 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1582 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1587 my $copy = $self->copy;
1589 my $stat = $copy->status if ref $copy->status;
1590 my $loc = $copy->location if ref $copy->location;
1591 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1593 $copy->status($stat->id) if $stat;
1594 $copy->location($loc->id) if $loc;
1595 $copy->circ_lib($circ_lib->id) if $circ_lib;
1596 $copy->editor($self->editor->requestor->id);
1597 $copy->edit_date('now');
1598 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1600 return $self->bail_on_events($self->editor->event)
1601 unless $self->editor->update_asset_copy($self->copy);
1603 $copy->status($U->copy_status($copy->status));
1604 $copy->location($loc) if $loc;
1605 $copy->circ_lib($circ_lib) if $circ_lib;
1608 sub update_reservation {
1610 my $reservation = $self->reservation;
1612 my $usr = $reservation->usr;
1613 my $target_rt = $reservation->target_resource_type;
1614 my $target_r = $reservation->target_resource;
1615 my $current_r = $reservation->current_resource;
1617 $reservation->usr($usr->id) if ref $usr;
1618 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1619 $reservation->target_resource($target_r->id) if ref $target_r;
1620 $reservation->current_resource($current_r->id) if ref $current_r;
1622 return $self->bail_on_events($self->editor->event)
1623 unless $self->editor->update_booking_reservation($self->reservation);
1626 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1627 $self->reservation($reservation);
1631 sub bail_on_events {
1632 my( $self, @evts ) = @_;
1633 $self->push_events(@evts);
1638 # ------------------------------------------------------------------------------
1639 # When an item is checked out, see if we can fulfill a hold for this patron
1640 # ------------------------------------------------------------------------------
1641 sub handle_checkout_holds {
1643 my $copy = $self->copy;
1644 my $patron = $self->patron;
1646 my $e = $self->editor;
1647 $self->fulfilled_holds([]);
1649 # pre/non-cats can't fulfill a hold
1650 return if $self->is_precat or $self->is_noncat;
1652 my $hold = $e->search_action_hold_request({
1653 current_copy => $copy->id ,
1654 cancel_time => undef,
1655 fulfillment_time => undef,
1657 {expire_time => undef},
1658 {expire_time => {'>' => 'now'}}
1662 if($hold and $hold->usr != $patron->id) {
1663 # reset the hold since the copy is now checked out
1665 $logger->info("circulator: un-targeting hold ".$hold->id.
1666 " because copy ".$copy->id." is getting checked out");
1668 $hold->clear_prev_check_time;
1669 $hold->clear_current_copy;
1670 $hold->clear_capture_time;
1671 $hold->clear_shelf_time;
1672 $hold->clear_shelf_expire_time;
1673 $hold->clear_current_shelf_lib;
1675 return $self->bail_on_event($e->event)
1676 unless $e->update_action_hold_request($hold);
1682 $hold = $self->find_related_user_hold($copy, $patron) or return;
1683 $logger->info("circulator: found related hold to fulfill in checkout");
1686 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1688 # if the hold was never officially captured, capture it.
1689 $hold->current_copy($copy->id);
1690 $hold->capture_time('now') unless $hold->capture_time;
1691 $hold->fulfillment_time('now');
1692 $hold->fulfillment_staff($e->requestor->id);
1693 $hold->fulfillment_lib($self->circ_lib);
1695 return $self->bail_on_events($e->event)
1696 unless $e->update_action_hold_request($hold);
1698 $holdcode->delete_hold_copy_maps($e, $hold->id);
1699 return $self->fulfilled_holds([$hold->id]);
1703 # ------------------------------------------------------------------------------
1704 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1705 # the patron directly targets the checked out item, see if there is another hold
1706 # for the patron that could be fulfilled by the checked out item. Fulfill the
1707 # oldest hold and only fulfill 1 of them.
1709 # For "another hold":
1711 # First, check for one that the copy matches via hold_copy_map, ensuring that
1712 # *any* hold type that this copy could fill may end up filled.
1714 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1715 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1716 # that are non-requestable to count as capturing those hold types.
1717 # ------------------------------------------------------------------------------
1718 sub find_related_user_hold {
1719 my($self, $copy, $patron) = @_;
1720 my $e = $self->editor;
1722 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1724 return undef unless $U->ou_ancestor_setting_value(
1725 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1727 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1729 select => {ahr => ['id']},
1738 fkey => 'current_copy',
1739 type => 'left' # there may be no current_copy
1746 fulfillment_time => undef,
1747 cancel_time => undef,
1749 {expire_time => undef},
1750 {expire_time => {'>' => 'now'}}
1754 target_copy => $self->copy->id
1758 {id => undef}, # left-join copy may be nonexistent
1759 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1763 order_by => {ahr => {request_time => {direction => 'asc'}}},
1767 my $hold_info = $e->json_query($args)->[0];
1768 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1769 return undef if $U->ou_ancestor_setting_value(
1770 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1772 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1774 select => {ahr => ['id']},
1779 fkey => 'current_copy',
1780 type => 'left' # there may be no current_copy
1787 fulfillment_time => undef,
1788 cancel_time => undef,
1790 {expire_time => undef},
1791 {expire_time => {'>' => 'now'}}
1798 target => $self->volume->id
1804 target => $self->title->id
1810 {id => undef}, # left-join copy may be nonexistent
1811 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1815 order_by => {ahr => {request_time => {direction => 'asc'}}},
1819 $hold_info = $e->json_query($args)->[0];
1820 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1825 sub run_checkout_scripts {
1830 my $runner = $self->script_runner;
1839 my $hard_due_date_name;
1841 if(!$self->legacy_script_support) {
1842 $self->run_indb_circ_test();
1843 $duration = $self->circ_matrix_matchpoint->duration_rule;
1844 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1845 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1846 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1850 $runner->load($self->circ_duration);
1852 my $result = $runner->run or
1853 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1855 $duration_name = $result->{durationRule};
1856 $recurring_name = $result->{recurringFinesRule};
1857 $max_fine_name = $result->{maxFine};
1858 $hard_due_date_name = $result->{hardDueDate};
1861 $duration_name = $duration->name if $duration;
1862 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1865 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1866 return $self->bail_on_events($evt) if ($evt && !$nobail);
1868 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1869 return $self->bail_on_events($evt) if ($evt && !$nobail);
1871 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1872 return $self->bail_on_events($evt) if ($evt && !$nobail);
1874 if($hard_due_date_name) {
1875 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1876 return $self->bail_on_events($evt) if ($evt && !$nobail);
1882 # The item circulates with an unlimited duration
1886 $hard_due_date = undef;
1889 $self->duration_rule($duration);
1890 $self->recurring_fines_rule($recurring);
1891 $self->max_fine_rule($max_fine);
1892 $self->hard_due_date($hard_due_date);
1896 sub build_checkout_circ_object {
1899 my $circ = Fieldmapper::action::circulation->new;
1900 my $duration = $self->duration_rule;
1901 my $max = $self->max_fine_rule;
1902 my $recurring = $self->recurring_fines_rule;
1903 my $hard_due_date = $self->hard_due_date;
1904 my $copy = $self->copy;
1905 my $patron = $self->patron;
1906 my $duration_date_ceiling;
1907 my $duration_date_ceiling_force;
1911 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1912 $duration_date_ceiling = $policy->{duration_date_ceiling};
1913 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1915 my $dname = $duration->name;
1916 my $mname = $max->name;
1917 my $rname = $recurring->name;
1919 if($hard_due_date) {
1920 $hdname = $hard_due_date->name;
1923 $logger->debug("circulator: building circulation ".
1924 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1926 $circ->duration($policy->{duration});
1927 $circ->recurring_fine($policy->{recurring_fine});
1928 $circ->duration_rule($duration->name);
1929 $circ->recurring_fine_rule($recurring->name);
1930 $circ->max_fine_rule($max->name);
1931 $circ->max_fine($policy->{max_fine});
1932 $circ->fine_interval($recurring->recurrence_interval);
1933 $circ->renewal_remaining($duration->max_renewals);
1934 $circ->grace_period($policy->{grace_period});
1938 $logger->info("circulator: copy found with an unlimited circ duration");
1939 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1940 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1941 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1942 $circ->renewal_remaining(0);
1943 $circ->grace_period(0);
1946 $circ->target_copy( $copy->id );
1947 $circ->usr( $patron->id );
1948 $circ->circ_lib( $self->circ_lib );
1949 $circ->workstation($self->editor->requestor->wsid)
1950 if defined $self->editor->requestor->wsid;
1952 # renewals maintain a link to the parent circulation
1953 $circ->parent_circ($self->parent_circ);
1955 if( $self->is_renewal ) {
1956 $circ->opac_renewal('t') if $self->opac_renewal;
1957 $circ->phone_renewal('t') if $self->phone_renewal;
1958 $circ->desk_renewal('t') if $self->desk_renewal;
1959 $circ->renewal_remaining($self->renewal_remaining);
1960 $circ->circ_staff($self->editor->requestor->id);
1964 # if the user provided an overiding checkout time,
1965 # (e.g. the checkout really happened several hours ago), then
1966 # we apply that here. Does this need a perm??
1967 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1968 if $self->checkout_time;
1970 # if a patron is renewing, 'requestor' will be the patron
1971 $circ->circ_staff($self->editor->requestor->id);
1972 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1977 sub do_reservation_pickup {
1980 $self->log_me("do_reservation_pickup()");
1982 $self->reservation->pickup_time('now');
1985 $self->reservation->current_resource &&
1986 $U->is_true($self->reservation->target_resource_type->catalog_item)
1988 # We used to try to set $self->copy and $self->patron here,
1989 # but that should already be done.
1991 $self->run_checkout_scripts(1);
1993 my $duration = $self->duration_rule;
1994 my $max = $self->max_fine_rule;
1995 my $recurring = $self->recurring_fines_rule;
1997 if ($duration && $max && $recurring) {
1998 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2000 my $dname = $duration->name;
2001 my $mname = $max->name;
2002 my $rname = $recurring->name;
2004 $logger->debug("circulator: updating reservation ".
2005 "with duration=$dname, maxfine=$mname, recurring=$rname");
2007 $self->reservation->fine_amount($policy->{recurring_fine});
2008 $self->reservation->max_fine($policy->{max_fine});
2009 $self->reservation->fine_interval($recurring->recurrence_interval);
2012 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2013 $self->update_copy();
2016 $self->reservation->fine_amount(
2017 $self->reservation->target_resource_type->fine_amount
2019 $self->reservation->max_fine(
2020 $self->reservation->target_resource_type->max_fine
2022 $self->reservation->fine_interval(
2023 $self->reservation->target_resource_type->fine_interval
2027 $self->update_reservation();
2030 sub do_reservation_return {
2032 my $request = shift;
2034 $self->log_me("do_reservation_return()");
2036 if (not ref $self->reservation) {
2037 my ($reservation, $evt) =
2038 $U->fetch_booking_reservation($self->reservation);
2039 return $self->bail_on_events($evt) if $evt;
2040 $self->reservation($reservation);
2043 $self->generate_fines(1);
2044 $self->reservation->return_time('now');
2045 $self->update_reservation();
2046 $self->reshelve_copy if $self->copy;
2048 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2049 $self->copy( $self->reservation->current_resource->catalog_item );
2053 sub booking_adjusted_due_date {
2055 my $circ = $self->circ;
2056 my $copy = $self->copy;
2058 return undef unless $self->use_booking;
2062 if( $self->due_date ) {
2064 return $self->bail_on_events($self->editor->event)
2065 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2067 $circ->due_date(cleanse_ISO8601($self->due_date));
2071 return unless $copy and $circ->due_date;
2074 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2075 if (@$booking_items) {
2076 my $booking_item = $booking_items->[0];
2077 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2079 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2080 my $shorten_circ_setting = $resource_type->elbow_room ||
2081 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2084 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2085 my $bookings = $booking_ses->request(
2086 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2087 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2089 $booking_ses->disconnect;
2091 my $dt_parser = DateTime::Format::ISO8601->new;
2092 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2094 for my $bid (@$bookings) {
2096 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2098 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2099 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2101 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2102 if ($booking_start < DateTime->now);
2105 if ($U->is_true($stop_circ_setting)) {
2106 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2108 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2109 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2112 # We set the circ duration here only to affect the logic that will
2113 # later (in a DB trigger) mangle the time part of the due date to
2114 # 11:59pm. Having any circ duration that is not a whole number of
2115 # days is enough to prevent the "correction."
2116 my $new_circ_duration = $due_date->epoch - time;
2117 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2118 $circ->duration("$new_circ_duration seconds");
2120 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2124 return $self->bail_on_events($self->editor->event)
2125 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2131 sub apply_modified_due_date {
2133 my $shift_earlier = shift;
2134 my $circ = $self->circ;
2135 my $copy = $self->copy;
2137 if( $self->due_date ) {
2139 return $self->bail_on_events($self->editor->event)
2140 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2142 $circ->due_date(cleanse_ISO8601($self->due_date));
2146 # if the due_date lands on a day when the location is closed
2147 return unless $copy and $circ->due_date;
2149 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2151 # due-date overlap should be determined by the location the item
2152 # is checked out from, not the owning or circ lib of the item
2153 my $org = $self->circ_lib;
2155 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2156 " with an item due date of ".$circ->due_date );
2158 my $dateinfo = $U->storagereq(
2159 'open-ils.storage.actor.org_unit.closed_date.overlap',
2160 $org, $circ->due_date );
2163 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2164 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2166 # XXX make the behavior more dynamic
2167 # for now, we just push the due date to after the close date
2168 if ($shift_earlier) {
2169 $circ->due_date($dateinfo->{start});
2171 $circ->due_date($dateinfo->{end});
2179 sub create_due_date {
2180 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2182 # if there is a raw time component (e.g. from postgres),
2183 # turn it into an interval that interval_to_seconds can parse
2184 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2186 # for now, use the server timezone. TODO: use workstation org timezone
2187 my $due_date = DateTime->now(time_zone => 'local');
2189 # add the circ duration
2190 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2193 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2194 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2195 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2200 # return ISO8601 time with timezone
2201 return $due_date->strftime('%FT%T%z');
2206 sub make_precat_copy {
2208 my $copy = $self->copy;
2211 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2213 $copy->editor($self->editor->requestor->id);
2214 $copy->edit_date('now');
2215 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2216 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2217 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2218 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2219 $self->update_copy();
2223 $logger->info("circulator: Creating a new precataloged ".
2224 "copy in checkout with barcode " . $self->copy_barcode);
2226 $copy = Fieldmapper::asset::copy->new;
2227 $copy->circ_lib($self->circ_lib);
2228 $copy->creator($self->editor->requestor->id);
2229 $copy->editor($self->editor->requestor->id);
2230 $copy->barcode($self->copy_barcode);
2231 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2232 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2233 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2235 $copy->dummy_title($self->dummy_title || "");
2236 $copy->dummy_author($self->dummy_author || "");
2237 $copy->dummy_isbn($self->dummy_isbn || "");
2238 $copy->circ_modifier($self->circ_modifier);
2241 # See if we need to override the circ_lib for the copy with a configured circ_lib
2242 # Setting is shortname of the org unit
2243 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2244 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2246 if($precat_circ_lib) {
2247 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2250 $self->bail_on_events($self->editor->event);
2254 $copy->circ_lib($org->id);
2258 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2260 $self->push_events($self->editor->event);
2264 # this is a little bit of a hack, but we need to
2265 # get the copy into the script runner
2266 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2270 sub checkout_noncat {
2276 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2277 my $count = $self->noncat_count || 1;
2278 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2280 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2284 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2285 $self->editor->requestor->id,
2293 $self->push_events($evt);
2301 # If a copy goes into transit and is then checked in before the transit checkin
2302 # interval has expired, push an event onto the overridable events list.
2303 sub check_transit_checkin_interval {
2306 # only concerned with in-transit items
2307 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2309 # no interval, no problem
2310 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2311 return unless $interval;
2313 # capture the transit so we don't have to fetch it again later during checkin
2315 $self->editor->search_action_transit_copy(
2316 {target_copy => $self->copy->id, dest_recv_time => undef}
2320 # transit from X to X for whatever reason has no min interval
2321 return if $self->transit->source == $self->transit->dest;
2323 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2324 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2325 my $horizon = $t_start->add(seconds => $seconds);
2327 # See if we are still within the transit checkin forbidden range
2328 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2329 if $horizon > DateTime->now;
2332 # Retarget local holds at checkin
2333 sub checkin_retarget {
2335 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2336 return unless $self->is_checkin; # Renewals need not be checked
2337 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2338 return if $self->is_precat; # No holds for precats
2339 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2340 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2341 my $status = $U->copy_status($self->copy->status);
2342 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2343 # Specifically target items that are likely new (by status ID)
2344 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2345 my $location = $self->copy->location;
2346 if(!ref($location)) {
2347 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2348 $self->copy->location($location);
2350 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2352 # Fetch holds for the bib
2353 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2354 $self->editor->authtoken,
2357 capture_time => undef, # No touching captured holds
2358 frozen => 'f', # Don't bother with frozen holds
2359 pickup_lib => $self->circ_lib # Only holds actually here
2362 # Error? Skip the step.
2363 return if exists $result->{"ilsevent"};
2367 foreach my $holdlist (keys %{$result}) {
2368 push @$holds, @{$result->{$holdlist}};
2371 return if scalar(@$holds) == 0; # No holds, no retargeting
2373 # Check for parts on this copy
2374 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2375 my %parts_hash = ();
2376 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2378 # Loop over holds in request-ish order
2379 # Stage 1: Get them into request-ish order
2380 # Also grab type and target for skipping low hanging ones
2381 $result = $self->editor->json_query({
2382 "select" => { "ahr" => ["id", "hold_type", "target"] },
2383 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2384 "where" => { "id" => $holds },
2386 { "class" => "pgt", "field" => "hold_priority"},
2387 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2388 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2389 { "class" => "ahr", "field" => "request_time"}
2394 if (ref $result eq "ARRAY" and scalar @$result) {
2395 foreach (@{$result}) {
2396 # Copy level, but not this copy?
2397 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2398 and $_->{target} != $self->copy->id);
2399 # Volume level, but not this volume?
2400 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2401 if(@$parts) { # We have parts?
2403 next if ($_->{hold_type} eq 'T');
2404 # Skip part holds for parts not on this copy
2405 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2407 # No parts, no part holds
2408 next if ($_->{hold_type} eq 'P');
2410 # So much for easy stuff, attempt a retarget!
2411 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2412 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2413 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2421 $self->log_me("do_checkin()");
2423 return $self->bail_on_events(
2424 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2427 $self->check_transit_checkin_interval;
2428 $self->checkin_retarget;
2430 # the renew code and mk_env should have already found our circulation object
2431 unless( $self->circ ) {
2433 my $circs = $self->editor->search_action_circulation(
2434 { target_copy => $self->copy->id, checkin_time => undef });
2436 $self->circ($$circs[0]);
2438 # for now, just warn if there are multiple open circs on a copy
2439 $logger->warn("circulator: we have ".scalar(@$circs).
2440 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2443 # run the fine generator against this circ, if this circ is there
2444 $self->generate_fines_start if $self->circ;
2446 if( $self->checkin_check_holds_shelf() ) {
2447 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2448 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2449 if($self->fake_hold_dest) {
2450 $self->hold->pickup_lib($self->circ_lib);
2452 $self->checkin_flesh_events;
2456 unless( $self->is_renewal ) {
2457 return $self->bail_on_events($self->editor->event)
2458 unless $self->editor->allowed('COPY_CHECKIN');
2461 $self->push_events($self->check_copy_alert());
2462 $self->push_events($self->check_checkin_copy_status());
2464 # if the circ is marked as 'claims returned', add the event to the list
2465 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2466 if ($self->circ and $self->circ->stop_fines
2467 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2469 $self->check_circ_deposit();
2471 # handle the overridable events
2472 $self->override_events unless $self->is_renewal;
2473 return if $self->bail_out;
2475 if( $self->copy and !$self->transit ) {
2477 $self->editor->search_action_transit_copy(
2478 { target_copy => $self->copy->id, dest_recv_time => undef }
2484 $self->generate_fines_finish;
2485 $self->checkin_handle_circ;
2486 return if $self->bail_out;
2487 $self->checkin_changed(1);
2489 } elsif( $self->transit ) {
2490 my $hold_transit = $self->process_received_transit;
2491 $self->checkin_changed(1);
2493 if( $self->bail_out ) {
2494 $self->checkin_flesh_events;
2498 if( my $e = $self->check_checkin_copy_status() ) {
2499 # If the original copy status is special, alert the caller
2500 my $ev = $self->events;
2501 $self->events([$e]);
2502 $self->override_events;
2503 return if $self->bail_out;
2507 if( $hold_transit or
2508 $U->copy_status($self->copy->status)->id
2509 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2512 if( $hold_transit ) {
2513 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2515 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2520 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2522 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2523 $self->reshelve_copy(1);
2524 $self->cancelled_hold_transit(1);
2525 $self->notify_hold(0); # don't notify for cancelled holds
2526 $self->fake_hold_dest(0);
2527 return if $self->bail_out;
2529 } elsif ($hold and $hold->hold_type eq 'R') {
2531 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2532 $self->notify_hold(0); # No need to notify
2533 $self->fake_hold_dest(0);
2534 $self->noop(1); # Don't try and capture for other holds/transits now
2535 $self->update_copy();
2536 $hold->fulfillment_time('now');
2537 $self->bail_on_events($self->editor->event)
2538 unless $self->editor->update_action_hold_request($hold);
2542 # hold transited to correct location
2543 if($self->fake_hold_dest) {
2544 $hold->pickup_lib($self->circ_lib);
2546 $self->checkin_flesh_events;
2551 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2553 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2554 " that is in-transit, but there is no transit.. repairing");
2555 $self->reshelve_copy(1);
2556 return if $self->bail_out;
2559 if( $self->is_renewal ) {
2560 $self->finish_fines_and_voiding;
2561 return if $self->bail_out;
2562 $self->push_events(OpenILS::Event->new('SUCCESS'));
2566 # ------------------------------------------------------------------------------
2567 # Circulations and transits are now closed where necessary. Now go on to see if
2568 # this copy can fulfill a hold or needs to be routed to a different location
2569 # ------------------------------------------------------------------------------
2571 my $needed_for_something = 0; # formerly "needed_for_hold"
2573 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2575 if (!$self->remote_hold) {
2576 if ($self->use_booking) {
2577 my $potential_hold = $self->hold_capture_is_possible;
2578 my $potential_reservation = $self->reservation_capture_is_possible;
2580 if ($potential_hold and $potential_reservation) {
2581 $logger->info("circulator: item could fulfill either hold or reservation");
2582 $self->push_events(new OpenILS::Event(
2583 "HOLD_RESERVATION_CONFLICT",
2584 "hold" => $potential_hold,
2585 "reservation" => $potential_reservation
2587 return if $self->bail_out;
2588 } elsif ($potential_hold) {
2589 $needed_for_something =
2590 $self->attempt_checkin_hold_capture;
2591 } elsif ($potential_reservation) {
2592 $needed_for_something =
2593 $self->attempt_checkin_reservation_capture;
2596 $needed_for_something = $self->attempt_checkin_hold_capture;
2599 return if $self->bail_out;
2601 unless($needed_for_something) {
2602 my $circ_lib = (ref $self->copy->circ_lib) ?
2603 $self->copy->circ_lib->id : $self->copy->circ_lib;
2605 if( $self->remote_hold ) {
2606 $circ_lib = $self->remote_hold->pickup_lib;
2607 $logger->warn("circulator: Copy ".$self->copy->barcode.
2608 " is on a remote hold's shelf, sending to $circ_lib");
2611 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2613 my $suppress_transit = 0;
2615 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2616 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2617 if($suppress_transit_source && $suppress_transit_source->{value}) {
2618 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2619 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2620 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2621 $suppress_transit = 1;
2626 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2627 # copy is where it needs to be, either for hold or reshelving
2629 $self->checkin_handle_precat();
2630 return if $self->bail_out;
2633 # copy needs to transit "home", or stick here if it's a floating copy
2635 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2636 $self->checkin_changed(1);
2637 $self->copy->circ_lib( $self->circ_lib );
2640 my $bc = $self->copy->barcode;
2641 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2642 $self->checkin_build_copy_transit($circ_lib);
2643 return if $self->bail_out;
2644 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2648 } else { # no-op checkin
2649 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2650 $self->checkin_changed(1);
2651 $self->copy->circ_lib( $self->circ_lib );
2656 if($self->claims_never_checked_out and
2657 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2659 # the item was not supposed to be checked out to the user and should now be marked as missing
2660 $self->copy->status(OILS_COPY_STATUS_MISSING);
2664 $self->reshelve_copy unless $needed_for_something;
2667 return if $self->bail_out;
2669 unless($self->checkin_changed) {
2671 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2672 my $stat = $U->copy_status($self->copy->status)->id;
2674 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2675 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2676 $self->bail_out(1); # no need to commit anything
2680 $self->push_events(OpenILS::Event->new('SUCCESS'))
2681 unless @{$self->events};
2684 $self->finish_fines_and_voiding;
2686 OpenILS::Utils::Penalty->calculate_penalties(
2687 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2689 $self->checkin_flesh_events;
2693 sub finish_fines_and_voiding {
2695 return unless $self->circ;
2697 # gather any updates to the circ after fine generation, if there was a circ
2698 $self->generate_fines_finish;
2700 return unless $self->backdate or $self->void_overdues;
2702 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2703 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2705 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2706 $self->editor, $self->circ, $self->backdate, $note);
2708 return $self->bail_on_events($evt) if $evt;
2710 # make sure the circ isn't closed if we just voided some fines
2711 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2712 return $self->bail_on_events($evt) if $evt;
2718 # if a deposit was payed for this item, push the event
2719 sub check_circ_deposit {
2721 return unless $self->circ;
2722 my $deposit = $self->editor->search_money_billing(
2724 xact => $self->circ->id,
2726 }, {idlist => 1})->[0];
2728 $self->push_events(OpenILS::Event->new(
2729 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2734 my $force = $self->force || shift;
2735 my $copy = $self->copy;
2737 my $stat = $U->copy_status($copy->status)->id;
2740 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2741 $stat != OILS_COPY_STATUS_CATALOGING and
2742 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2743 $stat != OILS_COPY_STATUS_RESHELVING )) {
2745 $copy->status( OILS_COPY_STATUS_RESHELVING );
2747 $self->checkin_changed(1);
2752 # Returns true if the item is at the current location
2753 # because it was transited there for a hold and the
2754 # hold has not been fulfilled
2755 sub checkin_check_holds_shelf {
2757 return 0 unless $self->copy;
2760 $U->copy_status($self->copy->status)->id ==
2761 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2763 # Attempt to clear shelf expired holds for this copy
2764 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2765 if($self->clear_expired);
2767 # find the hold that put us on the holds shelf
2768 my $holds = $self->editor->search_action_hold_request(
2770 current_copy => $self->copy->id,
2771 capture_time => { '!=' => undef },
2772 fulfillment_time => undef,
2773 cancel_time => undef,
2778 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2779 $self->reshelve_copy(1);
2783 my $hold = $$holds[0];
2785 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2786 $hold->id. "] for copy ".$self->copy->barcode);
2788 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2789 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2790 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2791 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2792 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2793 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2794 $self->fake_hold_dest(1);
2800 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2801 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2805 $logger->info("circulator: hold is not for here..");
2806 $self->remote_hold($hold);
2811 sub checkin_handle_precat {
2813 my $copy = $self->copy;
2815 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2816 $copy->status(OILS_COPY_STATUS_CATALOGING);
2817 $self->update_copy();
2818 $self->checkin_changed(1);
2819 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2824 sub checkin_build_copy_transit {
2827 my $copy = $self->copy;
2828 my $transit = Fieldmapper::action::transit_copy->new;
2830 # if we are transiting an item to the shelf shelf, it's a hold transit
2831 if (my $hold = $self->remote_hold) {
2832 $transit = Fieldmapper::action::hold_transit_copy->new;
2833 $transit->hold($hold->id);
2835 # the item is going into transit, remove any shelf-iness
2836 if ($hold->current_shelf_lib or $hold->shelf_time) {
2837 $hold->clear_current_shelf_lib;
2838 $hold->clear_shelf_time;
2839 return $self->bail_on_events($self->editor->event)
2840 unless $self->editor->update_action_hold_request($hold);
2844 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2845 $logger->info("circulator: transiting copy to $dest");
2847 $transit->source($self->circ_lib);
2848 $transit->dest($dest);
2849 $transit->target_copy($copy->id);
2850 $transit->source_send_time('now');
2851 $transit->copy_status( $U->copy_status($copy->status)->id );
2853 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2855 if ($self->remote_hold) {
2856 return $self->bail_on_events($self->editor->event)
2857 unless $self->editor->create_action_hold_transit_copy($transit);
2859 return $self->bail_on_events($self->editor->event)
2860 unless $self->editor->create_action_transit_copy($transit);
2863 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2865 $self->checkin_changed(1);
2869 sub hold_capture_is_possible {
2871 my $copy = $self->copy;
2873 # we've been explicitly told not to capture any holds
2874 return 0 if $self->capture eq 'nocapture';
2876 # See if this copy can fulfill any holds
2877 my $hold = $holdcode->find_nearest_permitted_hold(
2878 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2880 return undef if ref $hold eq "HASH" and
2881 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2885 sub reservation_capture_is_possible {
2887 my $copy = $self->copy;
2889 # we've been explicitly told not to capture any holds
2890 return 0 if $self->capture eq 'nocapture';
2892 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2893 my $resv = $booking_ses->request(
2894 "open-ils.booking.reservations.could_capture",
2895 $self->editor->authtoken, $copy->barcode
2897 $booking_ses->disconnect;
2898 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2899 $self->push_events($resv);
2905 # returns true if the item was used (or may potentially be used
2906 # in subsequent calls) to capture a hold.
2907 sub attempt_checkin_hold_capture {
2909 my $copy = $self->copy;
2911 # we've been explicitly told not to capture any holds
2912 return 0 if $self->capture eq 'nocapture';
2914 # See if this copy can fulfill any holds
2915 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2916 $self->editor, $copy, $self->editor->requestor );
2919 $logger->debug("circulator: no potential permitted".
2920 "holds found for copy ".$copy->barcode);
2924 if($self->capture ne 'capture') {
2925 # see if this item is in a hold-capture-delay location
2926 my $location = $self->copy->location;
2927 if(!ref($location)) {
2928 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2929 $self->copy->location($location);
2931 if($U->is_true($location->hold_verify)) {
2932 $self->bail_on_events(
2933 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2938 $self->retarget($retarget);
2940 my $suppress_transit = 0;
2941 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2942 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2943 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2944 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2945 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2946 $suppress_transit = 1;
2947 $self->hold->pickup_lib($self->circ_lib);
2952 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2954 $hold->current_copy($copy->id);
2955 $hold->capture_time('now');
2956 $self->put_hold_on_shelf($hold)
2957 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2959 # prevent DB errors caused by fetching
2960 # holds from storage, and updating through cstore
2961 $hold->clear_fulfillment_time;
2962 $hold->clear_fulfillment_staff;
2963 $hold->clear_fulfillment_lib;
2964 $hold->clear_expire_time;
2965 $hold->clear_cancel_time;
2966 $hold->clear_prev_check_time unless $hold->prev_check_time;
2968 $self->bail_on_events($self->editor->event)
2969 unless $self->editor->update_action_hold_request($hold);
2971 $self->checkin_changed(1);
2973 return 0 if $self->bail_out;
2975 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2977 if ($hold->hold_type eq 'R') {
2978 $copy->status(OILS_COPY_STATUS_CATALOGING);
2979 $hold->fulfillment_time('now');
2980 $self->noop(1); # Block other transit/hold checks
2981 $self->bail_on_events($self->editor->event)
2982 unless $self->editor->update_action_hold_request($hold);
2984 # This hold was captured in the correct location
2985 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2986 $self->push_events(OpenILS::Event->new('SUCCESS'));
2988 #$self->do_hold_notify($hold->id);
2989 $self->notify_hold($hold->id);
2994 # Hold needs to be picked up elsewhere. Build a hold
2995 # transit and route the item.
2996 $self->checkin_build_hold_transit();
2997 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2998 return 0 if $self->bail_out;
2999 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3002 # make sure we save the copy status
3004 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3008 sub attempt_checkin_reservation_capture {
3010 my $copy = $self->copy;
3012 # we've been explicitly told not to capture any holds
3013 return 0 if $self->capture eq 'nocapture';
3015 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3016 my $evt = $booking_ses->request(
3017 "open-ils.booking.resources.capture_for_reservation",
3018 $self->editor->authtoken,
3020 1 # don't update copy - we probably have it locked
3022 $booking_ses->disconnect;
3024 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3026 "open-ils.booking.resources.capture_for_reservation " .
3027 "didn't return an event!"
3031 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3032 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3034 # not-transferable is an error event we'll pass on the user
3035 $logger->warn("reservation capture attempted against non-transferable item");
3036 $self->push_events($evt);
3038 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3039 # Re-retrieve copy as reservation capture may have changed
3040 # its status and whatnot.
3042 "circulator: booking capture win on copy " . $self->copy->id
3044 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3046 "circulator: changing copy " . $self->copy->id .
3047 "'s status from " . $self->copy->status . " to " .
3050 $self->copy->status($new_copy_status);
3053 $self->reservation($evt->{"payload"}->{"reservation"});
3055 if (exists $evt->{"payload"}->{"transit"}) {
3059 "org" => $evt->{"payload"}->{"transit"}->dest
3063 $self->checkin_changed(1);
3067 # other results are treated as "nothing to capture"
3071 sub do_hold_notify {
3072 my( $self, $holdid ) = @_;
3074 my $e = new_editor(xact => 1);
3075 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3077 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3078 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3080 $logger->info("circulator: running delayed hold notify process");
3082 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3083 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3085 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3086 hold_id => $holdid, requestor => $self->editor->requestor);
3088 $logger->debug("circulator: built hold notifier");
3090 if(!$notifier->event) {
3092 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3094 my $stat = $notifier->send_email_notify;
3095 if( $stat == '1' ) {
3096 $logger->info("circulator: hold notify succeeded for hold $holdid");
3100 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3103 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3107 sub retarget_holds {
3109 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3110 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3111 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3112 # no reason to wait for the return value
3116 sub checkin_build_hold_transit {
3119 my $copy = $self->copy;
3120 my $hold = $self->hold;
3121 my $trans = Fieldmapper::action::hold_transit_copy->new;
3123 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3125 $trans->hold($hold->id);
3126 $trans->source($self->circ_lib);
3127 $trans->dest($hold->pickup_lib);
3128 $trans->source_send_time("now");
3129 $trans->target_copy($copy->id);
3131 # when the copy gets to its destination, it will recover
3132 # this status - put it onto the holds shelf
3133 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3135 return $self->bail_on_events($self->editor->event)
3136 unless $self->editor->create_action_hold_transit_copy($trans);
3141 sub process_received_transit {
3143 my $copy = $self->copy;
3144 my $copyid = $self->copy->id;
3146 my $status_name = $U->copy_status($copy->status)->name;
3147 $logger->debug("circulator: attempting transit receive on ".
3148 "copy $copyid. Copy status is $status_name");
3150 my $transit = $self->transit;
3152 # Check if we are in a transit suppress range
3153 my $suppress_transit = 0;
3154 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3155 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3156 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3157 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3158 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3159 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3160 $suppress_transit = 1;
3161 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3165 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3166 # - this item is in-transit to a different location
3167 # - Or we are capturing holds as transits, so why create a new transit?
3169 my $tid = $transit->id;
3170 my $loc = $self->circ_lib;
3171 my $dest = $transit->dest;
3173 $logger->info("circulator: Fowarding transit on copy which is destined ".
3174 "for a different location. transit=$tid, copy=$copyid, current ".
3175 "location=$loc, destination location=$dest");
3177 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3179 # grab the associated hold object if available
3180 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3181 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3183 return $self->bail_on_events($evt);
3186 # The transit is received, set the receive time
3187 $transit->dest_recv_time('now');
3188 $self->bail_on_events($self->editor->event)
3189 unless $self->editor->update_action_transit_copy($transit);
3191 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3193 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3194 $copy->status( $transit->copy_status );
3195 $self->update_copy();
3196 return if $self->bail_out;
3200 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3202 # hold has arrived at destination, set shelf time
3203 $self->put_hold_on_shelf($hold);
3204 $self->bail_on_events($self->editor->event)
3205 unless $self->editor->update_action_hold_request($hold);
3206 return if $self->bail_out;
3208 $self->notify_hold($hold_transit->hold);
3213 OpenILS::Event->new(
3216 payload => { transit => $transit, holdtransit => $hold_transit } ));
3218 return $hold_transit;
3222 # ------------------------------------------------------------------
3223 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3224 # ------------------------------------------------------------------
3225 sub put_hold_on_shelf {
3226 my($self, $hold) = @_;
3227 $hold->shelf_time('now');
3228 $hold->current_shelf_lib($self->circ_lib);
3229 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3235 sub generate_fines {
3237 my $reservation = shift;
3239 $self->generate_fines_start($reservation);
3240 $self->generate_fines_finish($reservation);
3245 sub generate_fines_start {
3247 my $reservation = shift;
3248 my $dt_parser = DateTime::Format::ISO8601->new;
3250 my $obj = $reservation ? $self->reservation : $self->circ;
3252 # If we have a grace period
3253 if($obj->can('grace_period')) {
3254 # Parse out the due date
3255 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3256 # Add the grace period to the due date
3257 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3258 # Don't generate fines on circs still in grace period
3259 return undef if ($due_date > DateTime->now);
3262 if (!exists($self->{_gen_fines_req})) {
3263 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3265 'open-ils.storage.action.circulation.overdue.generate_fines',
3273 sub generate_fines_finish {
3275 my $reservation = shift;
3277 return undef unless $self->{_gen_fines_req};
3279 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3281 $self->{_gen_fines_req}->wait_complete;
3282 delete($self->{_gen_fines_req});
3284 # refresh the circ in case the fine generator set the stop_fines field
3285 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3286 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3291 sub checkin_handle_circ {
3293 my $circ = $self->circ;
3294 my $copy = $self->copy;
3298 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3300 # backdate the circ if necessary
3301 if($self->backdate) {
3302 my $evt = $self->checkin_handle_backdate;
3303 return $self->bail_on_events($evt) if $evt;
3306 if(!$circ->stop_fines) {
3307 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3308 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3309 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3310 $circ->stop_fines_time('now');
3311 $circ->stop_fines_time($self->backdate) if $self->backdate;
3314 # Set the checkin vars since we have the item
3315 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3317 # capture the true scan time for back-dated checkins
3318 $circ->checkin_scan_time('now');
3320 $circ->checkin_staff($self->editor->requestor->id);
3321 $circ->checkin_lib($self->circ_lib);
3322 $circ->checkin_workstation($self->editor->requestor->wsid);
3324 my $circ_lib = (ref $self->copy->circ_lib) ?
3325 $self->copy->circ_lib->id : $self->copy->circ_lib;
3326 my $stat = $U->copy_status($self->copy->status)->id;
3328 if ($stat == OILS_COPY_STATUS_LOST) {
3329 # we will now handle lost fines, but the copy will retain its 'lost'
3330 # status if it needs to transit home unless lost_immediately_available
3333 # if we decide to also delay fine handling until the item arrives home,
3334 # we will need to call lost fine handling code both when checking items
3335 # in and also when receiving transits
3336 $self->checkin_handle_lost($circ_lib);
3337 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3338 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3340 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3345 # see if there are any fines owed on this circ. if not, close it
3346 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3347 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3349 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3351 return $self->bail_on_events($self->editor->event)
3352 unless $self->editor->update_action_circulation($circ);
3358 # ------------------------------------------------------------------
3359 # See if we need to void billings for lost checkin
3360 # ------------------------------------------------------------------
3361 sub checkin_handle_lost {
3363 my $circ_lib = shift;
3364 my $circ = $self->circ;
3366 my $max_return = $U->ou_ancestor_setting_value(
3367 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3372 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3373 $tm[5] -= 1 if $tm[5] > 0;
3374 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3376 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3377 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3379 $max_return = 0 if $today < $last_chance;
3382 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3384 my $void_lost = $U->ou_ancestor_setting_value(
3385 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3386 my $void_lost_fee = $U->ou_ancestor_setting_value(
3387 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3388 my $restore_od = $U->ou_ancestor_setting_value(
3389 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3390 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3391 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3393 $self->checkin_handle_lost_now_found(3) if $void_lost;
3394 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3395 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3398 if ($circ_lib != $self->circ_lib) {
3399 # if the item is not home, check to see if we want to retain the lost
3400 # status at this point in the process
3401 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3403 if ($immediately_available) {
3404 # lost item status does not need to be retained, so give it a
3405 # reshelving status as if it were a normal checkin
3406 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3409 $logger->info("circulator: not updating copy status on checkin because copy is lost");
3412 # lost item is home and processed, treat like a normal checkin from
3414 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3420 sub checkin_handle_backdate {
3423 # ------------------------------------------------------------------
3424 # clean up the backdate for date comparison
3425 # XXX We are currently taking the due-time from the original due-date,
3426 # not the input. Do we need to do this? This certainly interferes with
3427 # backdating of hourly checkouts, but that is likely a very rare case.
3428 # ------------------------------------------------------------------
3429 my $bd = cleanse_ISO8601($self->backdate);
3430 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3431 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3432 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3434 $self->backdate($bd);
3439 sub check_checkin_copy_status {
3441 my $copy = $self->copy;
3443 my $status = $U->copy_status($copy->status)->id;
3446 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3447 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3448 $status == OILS_COPY_STATUS_IN_PROCESS ||
3449 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3450 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3451 $status == OILS_COPY_STATUS_CATALOGING ||
3452 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3453 $status == OILS_COPY_STATUS_RESHELVING );
3455 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3456 if( $status == OILS_COPY_STATUS_LOST );
3458 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3459 if( $status == OILS_COPY_STATUS_MISSING );
3461 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3466 # --------------------------------------------------------------------------
3467 # On checkin, we need to return as many relevant objects as we can
3468 # --------------------------------------------------------------------------
3469 sub checkin_flesh_events {
3472 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3473 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3474 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3477 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3480 if($self->hold and !$self->hold->cancel_time) {
3481 $hold = $self->hold;
3482 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3486 # if we checked in a circulation, flesh the billing summary data
3487 $self->circ->billable_transaction(
3488 $self->editor->retrieve_money_billable_transaction([
3490 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3496 # flesh some patron fields before returning
3498 $self->editor->retrieve_actor_user([
3503 au => ['card', 'billing_address', 'mailing_address']
3510 for my $evt (@{$self->events}) {
3513 $payload->{copy} = $U->unflesh_copy($self->copy);
3514 $payload->{volume} = $self->volume;
3515 $payload->{record} = $record,
3516 $payload->{circ} = $self->circ;
3517 $payload->{transit} = $self->transit;
3518 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3519 $payload->{hold} = $hold;
3520 $payload->{patron} = $self->patron;
3521 $payload->{reservation} = $self->reservation
3522 unless (not $self->reservation or $self->reservation->cancel_time);
3524 $evt->{payload} = $payload;
3529 my( $self, $msg ) = @_;
3530 my $bc = ($self->copy) ? $self->copy->barcode :
3533 my $usr = ($self->patron) ? $self->patron->id : "";
3534 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3535 ", recipient=$usr, copy=$bc");
3541 $self->log_me("do_renew()");
3543 # Make sure there is an open circ to renew that is not
3544 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3545 my $usrid = $self->patron->id if $self->patron;
3546 my $circ = $self->editor->search_action_circulation({
3547 target_copy => $self->copy->id,
3548 xact_finish => undef,
3549 checkin_time => undef,
3550 ($usrid ? (usr => $usrid) : ()),
3552 {stop_fines => undef},
3553 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3557 return $self->bail_on_events($self->editor->event) unless $circ;
3559 # A user is not allowed to renew another user's items without permission
3560 unless( $circ->usr eq $self->editor->requestor->id ) {
3561 return $self->bail_on_events($self->editor->events)
3562 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3565 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3566 if $circ->renewal_remaining < 1;
3568 # -----------------------------------------------------------------
3570 $self->parent_circ($circ->id);
3571 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3574 # Opac renewal - re-use circ library from original circ (unless told not to)
3575 if($self->opac_renewal) {
3576 unless(defined($opac_renewal_use_circ_lib)) {
3577 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3578 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3579 $opac_renewal_use_circ_lib = 1;
3582 $opac_renewal_use_circ_lib = 0;
3585 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3588 # Run the fine generator against the old circ
3589 $self->generate_fines_start;
3591 $self->run_renew_permit;
3594 $self->do_checkin();
3595 return if $self->bail_out;
3597 unless( $self->permit_override ) {
3599 return if $self->bail_out;
3600 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3601 $self->remove_event('ITEM_NOT_CATALOGED');
3604 $self->override_events;
3605 return if $self->bail_out;
3608 $self->do_checkout();
3613 my( $self, $evt ) = @_;
3614 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3615 $logger->debug("circulator: removing event from list: $evt");
3616 my @events = @{$self->events};
3617 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3622 my( $self, $evt ) = @_;
3623 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3624 return grep { $_->{textcode} eq $evt } @{$self->events};
3629 sub run_renew_permit {
3632 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3633 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3634 $self->editor, $self->copy, $self->editor->requestor, 1
3636 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3639 if(!$self->legacy_script_support) {
3640 my $results = $self->run_indb_circ_test;
3641 $self->push_events($self->matrix_test_result_events)
3642 unless $self->circ_test_success;
3645 my $runner = $self->script_runner;
3647 $runner->load($self->circ_permit_renew);
3648 my $result = $runner->run or
3649 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3650 if ($result->{"events"}) {
3652 map { new OpenILS::Event($_) } @{$result->{"events"}}
3655 "circulator: circ_permit_renew for user " .
3656 $self->patron->id . " returned " .
3657 scalar(@{$result->{"events"}}) . " event(s)"
3661 $self->mk_script_runner;
3664 $logger->debug("circulator: re-creating script runner to be safe");
3668 # XXX: The primary mechanism for storing circ history is now handled
3669 # by tracking real circulation objects instead of bibs in a bucket.
3670 # However, this code is disabled by default and could be useful
3671 # some day, so may as well leave it for now.
3672 sub append_reading_list {
3676 $self->is_checkout and
3682 # verify history is globally enabled and uses the bucket mechanism
3683 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3684 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3686 return undef unless $htype and $htype eq 'bucket';
3688 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3690 # verify the patron wants to retain the hisory
3691 my $setting = $e->search_actor_user_setting(
3692 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3694 unless($setting and $setting->value) {
3699 my $bkt = $e->search_container_copy_bucket(
3700 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3705 # find the next item position
3706 my $last_item = $e->search_container_copy_bucket_item(
3707 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3708 $pos = $last_item->pos + 1 if $last_item;
3711 # create the history bucket if necessary
3712 $bkt = Fieldmapper::container::copy_bucket->new;
3713 $bkt->owner($self->patron->id);
3715 $bkt->btype('circ_history');
3717 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3720 my $item = Fieldmapper::container::copy_bucket_item->new;
3722 $item->bucket($bkt->id);
3723 $item->target_copy($self->copy->id);
3726 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3733 sub make_trigger_events {
3735 return unless $self->circ;
3736 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3737 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3738 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3743 sub checkin_handle_lost_now_found {
3744 my ($self, $bill_type) = @_;
3746 # ------------------------------------------------------------------
3747 # remove charge from patron's account if lost item is returned
3748 # ------------------------------------------------------------------
3750 my $bills = $self->editor->search_money_billing(
3752 xact => $self->circ->id,
3757 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3758 for my $bill (@$bills) {
3759 if( !$U->is_true($bill->voided) ) {
3760 $logger->info("lost item returned - voiding bill ".$bill->id);
3762 $bill->void_time('now');
3763 $bill->voider($self->editor->requestor->id);
3764 my $note = ($bill->note) ? $bill->note . "\n" : '';
3765 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3767 $self->bail_on_events($self->editor->event)
3768 unless $self->editor->update_money_billing($bill);
3773 sub checkin_handle_lost_now_found_restore_od {
3775 my $circ_lib = shift;
3777 # ------------------------------------------------------------------
3778 # restore those overdue charges voided when item was set to lost
3779 # ------------------------------------------------------------------
3781 my $ods = $self->editor->search_money_billing(
3783 xact => $self->circ->id,
3788 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3789 for my $bill (@$ods) {
3790 if( $U->is_true($bill->voided) ) {
3791 $logger->info("lost item returned - restoring overdue ".$bill->id);
3793 $bill->clear_void_time;
3794 $bill->voider($self->editor->requestor->id);
3795 my $note = ($bill->note) ? $bill->note . "\n" : '';
3796 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3798 $self->bail_on_events($self->editor->event)
3799 unless $self->editor->update_money_billing($bill);
3804 # ------------------------------------------------------------------
3805 # Lost-then-found item checked in. This sub generates new overdue
3806 # fines, beyond the point of any existing and possibly voided
3807 # overdue fines, up to the point of final checkin time (or max fine
3809 # ------------------------------------------------------------------
3810 sub generate_lost_overdue_fines {
3812 my $circ = $self->circ;
3813 my $e = $self->editor;
3815 # Re-open the transaction so the fine generator can see it
3816 if($circ->xact_finish or $circ->stop_fines) {
3818 $circ->clear_xact_finish;
3819 $circ->clear_stop_fines;
3820 $circ->clear_stop_fines_time;
3821 $e->update_action_circulation($circ) or return $e->die_event;
3825 $e->xact_begin; # generate_fines expects an in-xact editor
3826 $self->generate_fines;
3827 $circ = $self->circ; # generate fines re-fetches the circ
3831 # Re-close the transaction if no money is owed
3832 my ($obt) = $U->fetch_mbts($circ->id, $e);
3833 if ($obt and $obt->balance_owed == 0) {
3834 $circ->xact_finish('now');
3838 # Set stop fines if the fine generator didn't have to
3839 unless($circ->stop_fines) {
3840 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3841 $circ->stop_fines_time('now');
3845 # update the event data sent to the caller within the transaction
3846 $self->checkin_flesh_events;
3849 $e->update_action_circulation($circ) or return $e->die_event;