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 ) = @_;
195 translate_legacy_args($args);
196 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
197 my $api = $self->api_name;
200 OpenILS::Application::Circ::Circulator->new($auth, %$args);
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 ) = @_;
575 $class = ref($class) || $class;
576 my $self = bless( {}, $class );
579 $self->editor(new_editor(xact => 1, authtoken => $auth));
581 unless( $self->editor->checkauth ) {
582 $self->bail_on_events($self->editor->event);
586 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
588 $self->$_($args{$_}) for keys %args;
591 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
593 # if this is a renewal, default to desk_renewal
594 $self->desk_renewal(1) unless
595 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
597 $self->capture('') unless $self->capture;
599 unless(%user_groups) {
600 my $gps = $self->editor->retrieve_all_permission_grp_tree;
601 %user_groups = map { $_->id => $_ } @$gps;
608 # --------------------------------------------------------------------------
609 # True if we should discontinue processing
610 # --------------------------------------------------------------------------
612 my( $self, $bool ) = @_;
613 if( defined $bool ) {
614 $logger->info("circulator: BAILING OUT") if $bool;
615 $self->{bail_out} = $bool;
617 return $self->{bail_out};
622 my( $self, @evts ) = @_;
625 $e->{payload} = $self->copy if
626 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
628 $logger->info("circulator: pushing event ".$e->{textcode});
629 push( @{$self->events}, $e ) unless
630 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
636 return '' if $self->skip_permit_key;
637 my $key = md5_hex( time() . rand() . "$$" );
638 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
639 return $self->permit_key($key);
642 sub check_permit_key {
644 return 1 if $self->skip_permit_key;
645 my $key = $self->permit_key;
646 return 0 unless $key;
647 my $k = "oils_permit_key_$key";
648 my $one = $self->cache_handle->get_cache($k);
649 $self->cache_handle->delete_cache($k);
650 return ($one) ? 1 : 0;
653 sub seems_like_reservation {
656 # Some words about the following method:
657 # 1) It requires the VIEW_USER permission, but that's not an
658 # issue, right, since all staff should have that?
659 # 2) It returns only one reservation at a time, even if an item can be
660 # and is currently overbooked. Hmmm....
661 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
662 my $result = $booking_ses->request(
663 "open-ils.booking.reservations.by_returnable_resource_barcode",
664 $self->editor->authtoken,
667 $booking_ses->disconnect;
669 return $self->bail_on_events($result) if defined $U->event_code($result);
672 $self->reservation(shift @$result);
680 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
681 sub save_trimmed_copy {
682 my ($self, $copy) = @_;
685 $self->volume($copy->call_number);
686 $self->title($self->volume->record);
687 $self->copy->call_number($self->volume->id);
688 $self->volume->record($self->title->id);
689 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
690 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
691 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
692 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
698 my $e = $self->editor;
700 # --------------------------------------------------------------------------
701 # Grab the fleshed copy
702 # --------------------------------------------------------------------------
703 unless($self->is_noncat) {
706 $copy = $e->retrieve_asset_copy(
707 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
709 } elsif( $self->copy_barcode ) {
711 $copy = $e->search_asset_copy(
712 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
713 } elsif( $self->reservation ) {
714 my $res = $e->json_query(
716 "select" => {"acp" => ["id"]},
721 "field" => "barcode",
725 "field" => "current_resource"
733 "id" => (ref $self->reservation) ?
734 $self->reservation->id : $self->reservation
739 if (ref $res eq "ARRAY" and scalar @$res) {
740 $logger->info("circulator: mapped reservation " .
741 $self->reservation . " to copy " . $res->[0]->{"id"});
742 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
747 $self->save_trimmed_copy($copy);
749 # We can't renew if there is no copy
750 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
751 if $self->is_renewal;
756 # --------------------------------------------------------------------------
758 # --------------------------------------------------------------------------
762 flesh_fields => {au => [ qw/ card / ]}
765 if( $self->patron_id ) {
766 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
767 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
769 } elsif( $self->patron_barcode ) {
771 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
772 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
773 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
775 $patron = $e->retrieve_actor_user($card->usr)
776 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
778 # Use the card we looked up, not the patron's primary, for card active checks
779 $patron->card($card);
782 if( my $copy = $self->copy ) {
785 $flesh->{flesh_fields}->{circ} = ['usr'];
787 my $circ = $e->search_action_circulation([
788 {target_copy => $copy->id, checkin_time => undef}, $flesh
792 $patron = $circ->usr;
793 $circ->usr($patron->id); # de-flesh for consistency
799 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
800 unless $self->patron($patron) or $self->is_checkin;
802 unless($self->is_checkin) {
804 # Check for inactivity and patron reg. expiration
806 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
807 unless $U->is_true($patron->active);
809 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
810 unless $U->is_true($patron->card->active);
812 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
813 cleanse_ISO8601($patron->expire_date));
815 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
816 if( CORE::time > $expire->epoch ) ;
820 # --------------------------------------------------------------------------
821 # This builds the script runner environment and fetches most of the
823 # --------------------------------------------------------------------------
824 sub mk_script_runner {
830 qw/copy copy_barcode copy_id patron
831 patron_id patron_barcode volume title editor/;
833 # Translate our objects into the ScriptBuilder args hash
834 $$args{$_} = $self->$_() for @fields;
836 $args->{ignore_user_status} = 1 if $self->is_checkin;
837 $$args{fetch_patron_by_circ_copy} = 1;
838 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
840 if( my $pco = $self->pending_checkouts ) {
841 $logger->info("circulator: we were given a pending checkouts number of $pco");
842 $$args{patronItemsOut} = $pco;
845 # This fetches most of the objects we need
846 $self->script_runner(
847 OpenILS::Application::Circ::ScriptBuilder->build($args));
849 # Now we translate the ScriptBuilder objects back into self
850 $self->$_($$args{$_}) for @fields;
852 my @evts = @{$args->{_events}} if $args->{_events};
854 $logger->debug("circulator: script builder returned events: @evts") if @evts;
858 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
859 if(!$self->is_noncat and
861 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
865 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
866 return $self->bail_on_events(@e);
871 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
872 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
873 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
874 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
878 # We can't renew if there is no copy
879 return $self->bail_on_events(@evts) if
880 $self->is_renewal and !$self->copy;
882 # Set some circ-specific flags in the script environment
883 my $evt = "environment";
884 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
886 if( $self->is_noncat ) {
887 $self->script_runner->insert("$evt.isNonCat", 1);
888 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
891 if( $self->is_precat ) {
892 $self->script_runner->insert("environment.isPrecat", 1, 1);
895 $self->script_runner->add_path( $_ ) for @$script_libs;
900 # --------------------------------------------------------------------------
901 # Does the circ permit work
902 # --------------------------------------------------------------------------
906 $self->log_me("do_permit()");
908 unless( $self->editor->requestor->id == $self->patron->id ) {
909 return $self->bail_on_events($self->editor->event)
910 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
913 $self->check_captured_holds();
914 $self->do_copy_checks();
915 return if $self->bail_out;
916 $self->run_patron_permit_scripts();
917 $self->run_copy_permit_scripts()
918 unless $self->is_precat or $self->is_noncat;
919 $self->check_item_deposit_events();
920 $self->override_events();
921 return if $self->bail_out;
923 if($self->is_precat and not $self->request_precat) {
926 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
927 return $self->bail_out(1) unless $self->is_renewal;
931 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
934 sub check_item_deposit_events {
936 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
937 if $self->is_deposit and not $self->is_deposit_exempt;
938 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
939 if $self->is_rental and not $self->is_rental_exempt;
942 # returns true if the user is not required to pay deposits
943 sub is_deposit_exempt {
945 my $pid = (ref $self->patron->profile) ?
946 $self->patron->profile->id : $self->patron->profile;
947 my $groups = $U->ou_ancestor_setting_value(
948 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
949 for my $grp (@$groups) {
950 return 1 if $self->is_group_descendant($grp, $pid);
955 # returns true if the user is not required to pay rental fees
956 sub is_rental_exempt {
958 my $pid = (ref $self->patron->profile) ?
959 $self->patron->profile->id : $self->patron->profile;
960 my $groups = $U->ou_ancestor_setting_value(
961 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
962 for my $grp (@$groups) {
963 return 1 if $self->is_group_descendant($grp, $pid);
968 sub is_group_descendant {
969 my($self, $p_id, $c_id) = @_;
970 return 0 unless defined $p_id and defined $c_id;
971 return 1 if $c_id == $p_id;
972 while(my $grp = $user_groups{$c_id}) {
973 $c_id = $grp->parent;
974 return 0 unless defined $c_id;
975 return 1 if $c_id == $p_id;
980 sub check_captured_holds {
982 my $copy = $self->copy;
983 my $patron = $self->patron;
985 return undef unless $copy;
987 my $s = $U->copy_status($copy->status)->id;
988 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
989 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
991 # Item is on the holds shelf, make sure it's going to the right person
992 my $holds = $self->editor->search_action_hold_request(
995 current_copy => $copy->id ,
996 capture_time => { '!=' => undef },
997 cancel_time => undef,
998 fulfillment_time => undef
1004 if( $holds and $$holds[0] ) {
1005 return undef if $$holds[0]->usr == $patron->id;
1008 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1010 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1014 sub do_copy_checks {
1016 my $copy = $self->copy;
1017 return unless $copy;
1019 my $stat = $U->copy_status($copy->status)->id;
1021 # We cannot check out a copy if it is in-transit
1022 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1023 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1026 $self->handle_claims_returned();
1027 return if $self->bail_out;
1029 # no claims returned circ was found, check if there is any open circ
1030 unless( $self->is_renewal ) {
1032 my $circs = $self->editor->search_action_circulation(
1033 { target_copy => $copy->id, checkin_time => undef }
1036 if(my $old_circ = $circs->[0]) { # an open circ was found
1038 my $payload = {copy => $copy};
1040 if($old_circ->usr == $self->patron->id) {
1042 $payload->{old_circ} = $old_circ;
1044 # If there is an open circulation on the checkout item and an auto-renew
1045 # interval is defined, inform the caller that they should go
1046 # ahead and renew the item instead of warning about open circulations.
1048 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1050 'circ.checkout_auto_renew_age',
1054 if($auto_renew_intvl) {
1055 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1056 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1058 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1059 $payload->{auto_renew} = 1;
1064 return $self->bail_on_events(
1065 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1071 my $LEGACY_CIRC_EVENT_MAP = {
1072 'no_item' => 'ITEM_NOT_CATALOGED',
1073 'actor.usr.barred' => 'PATRON_BARRED',
1074 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1075 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1076 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1077 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1078 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1079 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1080 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1081 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1085 # ---------------------------------------------------------------------
1086 # This pushes any patron-related events into the list but does not
1087 # set bail_out for any events
1088 # ---------------------------------------------------------------------
1089 sub run_patron_permit_scripts {
1091 my $runner = $self->script_runner;
1092 my $patronid = $self->patron->id;
1096 if(!$self->legacy_script_support) {
1098 my $results = $self->run_indb_circ_test;
1099 unless($self->circ_test_success) {
1100 # no_item result is OK during noncat checkout
1101 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1102 push @allevents, $self->matrix_test_result_events;
1108 # ---------------------------------------------------------------------
1109 # # Now run the patron permit script
1110 # ---------------------------------------------------------------------
1111 $runner->load($self->circ_permit_patron);
1112 my $result = $runner->run or
1113 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1115 my $patron_events = $result->{events};
1117 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1118 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1119 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1120 $penalties = $penalties->{fatal_penalties};
1122 for my $pen (@$penalties) {
1123 my $event = OpenILS::Event->new($pen->name);
1124 $event->{desc} = $pen->label;
1125 push(@allevents, $event);
1128 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1132 $_->{payload} = $self->copy if
1133 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1136 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1138 $self->push_events(@allevents);
1141 sub matrix_test_result_codes {
1143 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1146 sub matrix_test_result_events {
1149 my $event = new OpenILS::Event(
1150 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1152 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1154 } (@{$self->matrix_test_result});
1157 sub run_indb_circ_test {
1159 return $self->matrix_test_result if $self->matrix_test_result;
1161 my $dbfunc = ($self->is_renewal) ?
1162 'action.item_user_renew_test' : 'action.item_user_circ_test';
1164 if( $self->is_precat && $self->request_precat) {
1165 $self->make_precat_copy;
1166 return if $self->bail_out;
1169 my $results = $self->editor->json_query(
1173 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1179 $self->circ_test_success($U->is_true($results->[0]->{success}));
1181 if(my $mp = $results->[0]->{matchpoint}) {
1182 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1183 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1184 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1185 if(defined($results->[0]->{renewals})) {
1186 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1188 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1189 if(defined($results->[0]->{grace_period})) {
1190 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1192 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1193 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1194 # Grab the *last* response for limit_groups, where it is more likely to be filled
1195 $self->limit_groups($results->[-1]->{limit_groups});
1198 return $self->matrix_test_result($results);
1201 # ---------------------------------------------------------------------
1202 # given a use and copy, this will calculate the circulation policy
1203 # parameters. Only works with in-db circ.
1204 # ---------------------------------------------------------------------
1208 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1210 $self->run_indb_circ_test;
1213 circ_test_success => $self->circ_test_success,
1214 failure_events => [],
1215 failure_codes => [],
1216 matchpoint => $self->circ_matrix_matchpoint
1219 unless($self->circ_test_success) {
1220 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1221 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1224 if($self->circ_matrix_matchpoint) {
1225 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1226 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1227 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1228 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1230 my $policy = $self->get_circ_policy(
1231 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1233 $$results{$_} = $$policy{$_} for keys %$policy;
1239 # ---------------------------------------------------------------------
1240 # Loads the circ policy info for duration, recurring fine, and max
1241 # fine based on the current copy
1242 # ---------------------------------------------------------------------
1243 sub get_circ_policy {
1244 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1247 duration_rule => $duration_rule->name,
1248 recurring_fine_rule => $recurring_fine_rule->name,
1249 max_fine_rule => $max_fine_rule->name,
1250 max_fine => $self->get_max_fine_amount($max_fine_rule),
1251 fine_interval => $recurring_fine_rule->recurrence_interval,
1252 renewal_remaining => $duration_rule->max_renewals,
1253 grace_period => $recurring_fine_rule->grace_period
1256 if($hard_due_date) {
1257 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1258 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1261 $policy->{duration_date_ceiling} = undef;
1262 $policy->{duration_date_ceiling_force} = undef;
1265 $policy->{duration} = $duration_rule->shrt
1266 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1267 $policy->{duration} = $duration_rule->normal
1268 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1269 $policy->{duration} = $duration_rule->extended
1270 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1272 $policy->{recurring_fine} = $recurring_fine_rule->low
1273 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1274 $policy->{recurring_fine} = $recurring_fine_rule->normal
1275 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1276 $policy->{recurring_fine} = $recurring_fine_rule->high
1277 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1282 sub get_max_fine_amount {
1284 my $max_fine_rule = shift;
1285 my $max_amount = $max_fine_rule->amount;
1287 # if is_percent is true then the max->amount is
1288 # use as a percentage of the copy price
1289 if ($U->is_true($max_fine_rule->is_percent)) {
1290 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1291 $max_amount = $price * $max_fine_rule->amount / 100;
1293 $U->ou_ancestor_setting_value(
1295 'circ.max_fine.cap_at_price',
1299 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1300 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1308 sub run_copy_permit_scripts {
1310 my $copy = $self->copy || return;
1311 my $runner = $self->script_runner;
1315 if(!$self->legacy_script_support) {
1316 my $results = $self->run_indb_circ_test;
1317 push @allevents, $self->matrix_test_result_events
1318 unless $self->circ_test_success;
1321 # ---------------------------------------------------------------------
1322 # Capture all of the copy permit events
1323 # ---------------------------------------------------------------------
1324 $runner->load($self->circ_permit_copy);
1325 my $result = $runner->run or
1326 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1327 my $copy_events = $result->{events};
1329 # ---------------------------------------------------------------------
1330 # Now collect all of the events together
1331 # ---------------------------------------------------------------------
1332 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1335 # See if this copy has an alert message
1336 my $ae = $self->check_copy_alert();
1337 push( @allevents, $ae ) if $ae;
1339 # uniquify the events
1340 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1341 @allevents = values %hash;
1343 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1345 $self->push_events(@allevents);
1349 sub check_copy_alert {
1351 return undef if $self->is_renewal;
1352 return OpenILS::Event->new(
1353 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1354 if $self->copy and $self->copy->alert_message;
1360 # --------------------------------------------------------------------------
1361 # If the call is overriding and has permissions to override every collected
1362 # event, the are cleared. Any event that the caller does not have
1363 # permission to override, will be left in the event list and bail_out will
1365 # XXX We need code in here to cancel any holds/transits on copies
1366 # that are being force-checked out
1367 # --------------------------------------------------------------------------
1368 sub override_events {
1370 my @events = @{$self->events};
1371 return unless @events;
1372 my $oargs = $self->override_args;
1374 if(!$self->override) {
1375 return $self->bail_out(1)
1376 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1381 for my $e (@events) {
1382 my $tc = $e->{textcode};
1383 next if $tc eq 'SUCCESS';
1384 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1385 my $ov = "$tc.override";
1386 $logger->info("circulator: attempting to override event: $ov");
1388 return $self->bail_on_events($self->editor->event)
1389 unless( $self->editor->allowed($ov) );
1391 return $self->bail_out(1);
1397 # --------------------------------------------------------------------------
1398 # If there is an open claimsreturn circ on the requested copy, close the
1399 # circ if overriding, otherwise bail out
1400 # --------------------------------------------------------------------------
1401 sub handle_claims_returned {
1403 my $copy = $self->copy;
1405 my $CR = $self->editor->search_action_circulation(
1407 target_copy => $copy->id,
1408 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1409 checkin_time => undef,
1413 return unless ($CR = $CR->[0]);
1417 # - If the caller has set the override flag, we will check the item in
1418 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1420 $CR->checkin_time('now');
1421 $CR->checkin_scan_time('now');
1422 $CR->checkin_lib($self->circ_lib);
1423 $CR->checkin_workstation($self->editor->requestor->wsid);
1424 $CR->checkin_staff($self->editor->requestor->id);
1426 $evt = $self->editor->event
1427 unless $self->editor->update_action_circulation($CR);
1430 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1433 $self->bail_on_events($evt) if $evt;
1438 # --------------------------------------------------------------------------
1439 # This performs the checkout
1440 # --------------------------------------------------------------------------
1444 $self->log_me("do_checkout()");
1446 # make sure perms are good if this isn't a renewal
1447 unless( $self->is_renewal ) {
1448 return $self->bail_on_events($self->editor->event)
1449 unless( $self->editor->allowed('COPY_CHECKOUT') );
1452 # verify the permit key
1453 unless( $self->check_permit_key ) {
1454 if( $self->permit_override ) {
1455 return $self->bail_on_events($self->editor->event)
1456 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1458 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1462 # if this is a non-cataloged circ, build the circ and finish
1463 if( $self->is_noncat ) {
1464 $self->checkout_noncat;
1466 OpenILS::Event->new('SUCCESS',
1467 payload => { noncat_circ => $self->circ }));
1471 if( $self->is_precat ) {
1472 $self->make_precat_copy;
1473 return if $self->bail_out;
1475 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1476 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1479 $self->do_copy_checks;
1480 return if $self->bail_out;
1482 $self->run_checkout_scripts();
1483 return if $self->bail_out;
1485 $self->build_checkout_circ_object();
1486 return if $self->bail_out;
1488 my $modify_to_start = $self->booking_adjusted_due_date();
1489 return if $self->bail_out;
1491 $self->apply_modified_due_date($modify_to_start);
1492 return if $self->bail_out;
1494 return $self->bail_on_events($self->editor->event)
1495 unless $self->editor->create_action_circulation($self->circ);
1497 # refresh the circ to force local time zone for now
1498 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1500 if($self->limit_groups) {
1501 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1504 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1506 return if $self->bail_out;
1508 $self->apply_deposit_fee();
1509 return if $self->bail_out;
1511 $self->handle_checkout_holds();
1512 return if $self->bail_out;
1514 # ------------------------------------------------------------------------------
1515 # Update the patron penalty info in the DB. Run it for permit-overrides
1516 # since the penalties are not updated during the permit phase
1517 # ------------------------------------------------------------------------------
1518 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1520 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1523 if($self->is_renewal) {
1524 # flesh the billing summary for the checked-in circ
1525 $pcirc = $self->editor->retrieve_action_circulation([
1527 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1532 OpenILS::Event->new('SUCCESS',
1534 copy => $U->unflesh_copy($self->copy),
1535 volume => $self->volume,
1536 circ => $self->circ,
1538 holds_fulfilled => $self->fulfilled_holds,
1539 deposit_billing => $self->deposit_billing,
1540 rental_billing => $self->rental_billing,
1541 parent_circ => $pcirc,
1542 patron => ($self->return_patron) ? $self->patron : undef,
1543 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1549 sub apply_deposit_fee {
1551 my $copy = $self->copy;
1553 ($self->is_deposit and not $self->is_deposit_exempt) or
1554 ($self->is_rental and not $self->is_rental_exempt);
1556 return if $self->is_deposit and $self->skip_deposit_fee;
1557 return if $self->is_rental and $self->skip_rental_fee;
1559 my $bill = Fieldmapper::money::billing->new;
1560 my $amount = $copy->deposit_amount;
1564 if($self->is_deposit) {
1565 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1567 $self->deposit_billing($bill);
1569 $billing_type = OILS_BILLING_TYPE_RENTAL;
1571 $self->rental_billing($bill);
1574 $bill->xact($self->circ->id);
1575 $bill->amount($amount);
1576 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1577 $bill->billing_type($billing_type);
1578 $bill->btype($btype);
1579 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1581 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1586 my $copy = $self->copy;
1588 my $stat = $copy->status if ref $copy->status;
1589 my $loc = $copy->location if ref $copy->location;
1590 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1592 $copy->status($stat->id) if $stat;
1593 $copy->location($loc->id) if $loc;
1594 $copy->circ_lib($circ_lib->id) if $circ_lib;
1595 $copy->editor($self->editor->requestor->id);
1596 $copy->edit_date('now');
1597 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1599 return $self->bail_on_events($self->editor->event)
1600 unless $self->editor->update_asset_copy($self->copy);
1602 $copy->status($U->copy_status($copy->status));
1603 $copy->location($loc) if $loc;
1604 $copy->circ_lib($circ_lib) if $circ_lib;
1607 sub update_reservation {
1609 my $reservation = $self->reservation;
1611 my $usr = $reservation->usr;
1612 my $target_rt = $reservation->target_resource_type;
1613 my $target_r = $reservation->target_resource;
1614 my $current_r = $reservation->current_resource;
1616 $reservation->usr($usr->id) if ref $usr;
1617 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1618 $reservation->target_resource($target_r->id) if ref $target_r;
1619 $reservation->current_resource($current_r->id) if ref $current_r;
1621 return $self->bail_on_events($self->editor->event)
1622 unless $self->editor->update_booking_reservation($self->reservation);
1625 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1626 $self->reservation($reservation);
1630 sub bail_on_events {
1631 my( $self, @evts ) = @_;
1632 $self->push_events(@evts);
1637 # ------------------------------------------------------------------------------
1638 # When an item is checked out, see if we can fulfill a hold for this patron
1639 # ------------------------------------------------------------------------------
1640 sub handle_checkout_holds {
1642 my $copy = $self->copy;
1643 my $patron = $self->patron;
1645 my $e = $self->editor;
1646 $self->fulfilled_holds([]);
1648 # pre/non-cats can't fulfill a hold
1649 return if $self->is_precat or $self->is_noncat;
1651 my $hold = $e->search_action_hold_request({
1652 current_copy => $copy->id ,
1653 cancel_time => undef,
1654 fulfillment_time => undef,
1656 {expire_time => undef},
1657 {expire_time => {'>' => 'now'}}
1661 if($hold and $hold->usr != $patron->id) {
1662 # reset the hold since the copy is now checked out
1664 $logger->info("circulator: un-targeting hold ".$hold->id.
1665 " because copy ".$copy->id." is getting checked out");
1667 $hold->clear_prev_check_time;
1668 $hold->clear_current_copy;
1669 $hold->clear_capture_time;
1670 $hold->clear_shelf_time;
1671 $hold->clear_shelf_expire_time;
1672 $hold->clear_current_shelf_lib;
1674 return $self->bail_on_event($e->event)
1675 unless $e->update_action_hold_request($hold);
1681 $hold = $self->find_related_user_hold($copy, $patron) or return;
1682 $logger->info("circulator: found related hold to fulfill in checkout");
1685 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1687 # if the hold was never officially captured, capture it.
1688 $hold->current_copy($copy->id);
1689 $hold->capture_time('now') unless $hold->capture_time;
1690 $hold->fulfillment_time('now');
1691 $hold->fulfillment_staff($e->requestor->id);
1692 $hold->fulfillment_lib($self->circ_lib);
1694 return $self->bail_on_events($e->event)
1695 unless $e->update_action_hold_request($hold);
1697 $holdcode->delete_hold_copy_maps($e, $hold->id);
1698 return $self->fulfilled_holds([$hold->id]);
1702 # ------------------------------------------------------------------------------
1703 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1704 # the patron directly targets the checked out item, see if there is another hold
1705 # for the patron that could be fulfilled by the checked out item. Fulfill the
1706 # oldest hold and only fulfill 1 of them.
1708 # For "another hold":
1710 # First, check for one that the copy matches via hold_copy_map, ensuring that
1711 # *any* hold type that this copy could fill may end up filled.
1713 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1714 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1715 # that are non-requestable to count as capturing those hold types.
1716 # ------------------------------------------------------------------------------
1717 sub find_related_user_hold {
1718 my($self, $copy, $patron) = @_;
1719 my $e = $self->editor;
1721 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1723 return undef unless $U->ou_ancestor_setting_value(
1724 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1726 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1728 select => {ahr => ['id']},
1737 fkey => 'current_copy',
1738 type => 'left' # there may be no current_copy
1745 fulfillment_time => undef,
1746 cancel_time => undef,
1748 {expire_time => undef},
1749 {expire_time => {'>' => 'now'}}
1753 target_copy => $self->copy->id
1757 {id => undef}, # left-join copy may be nonexistent
1758 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1762 order_by => {ahr => {request_time => {direction => 'asc'}}},
1766 my $hold_info = $e->json_query($args)->[0];
1767 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1768 return undef if $U->ou_ancestor_setting_value(
1769 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1771 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1773 select => {ahr => ['id']},
1778 fkey => 'current_copy',
1779 type => 'left' # there may be no current_copy
1786 fulfillment_time => undef,
1787 cancel_time => undef,
1789 {expire_time => undef},
1790 {expire_time => {'>' => 'now'}}
1797 target => $self->volume->id
1803 target => $self->title->id
1809 {id => undef}, # left-join copy may be nonexistent
1810 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1814 order_by => {ahr => {request_time => {direction => 'asc'}}},
1818 $hold_info = $e->json_query($args)->[0];
1819 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1824 sub run_checkout_scripts {
1829 my $runner = $self->script_runner;
1838 my $hard_due_date_name;
1840 if(!$self->legacy_script_support) {
1841 $self->run_indb_circ_test();
1842 $duration = $self->circ_matrix_matchpoint->duration_rule;
1843 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1844 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1845 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1849 $runner->load($self->circ_duration);
1851 my $result = $runner->run or
1852 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1854 $duration_name = $result->{durationRule};
1855 $recurring_name = $result->{recurringFinesRule};
1856 $max_fine_name = $result->{maxFine};
1857 $hard_due_date_name = $result->{hardDueDate};
1860 $duration_name = $duration->name if $duration;
1861 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1864 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1865 return $self->bail_on_events($evt) if ($evt && !$nobail);
1867 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1868 return $self->bail_on_events($evt) if ($evt && !$nobail);
1870 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1871 return $self->bail_on_events($evt) if ($evt && !$nobail);
1873 if($hard_due_date_name) {
1874 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1875 return $self->bail_on_events($evt) if ($evt && !$nobail);
1881 # The item circulates with an unlimited duration
1885 $hard_due_date = undef;
1888 $self->duration_rule($duration);
1889 $self->recurring_fines_rule($recurring);
1890 $self->max_fine_rule($max_fine);
1891 $self->hard_due_date($hard_due_date);
1895 sub build_checkout_circ_object {
1898 my $circ = Fieldmapper::action::circulation->new;
1899 my $duration = $self->duration_rule;
1900 my $max = $self->max_fine_rule;
1901 my $recurring = $self->recurring_fines_rule;
1902 my $hard_due_date = $self->hard_due_date;
1903 my $copy = $self->copy;
1904 my $patron = $self->patron;
1905 my $duration_date_ceiling;
1906 my $duration_date_ceiling_force;
1910 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1911 $duration_date_ceiling = $policy->{duration_date_ceiling};
1912 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1914 my $dname = $duration->name;
1915 my $mname = $max->name;
1916 my $rname = $recurring->name;
1918 if($hard_due_date) {
1919 $hdname = $hard_due_date->name;
1922 $logger->debug("circulator: building circulation ".
1923 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1925 $circ->duration($policy->{duration});
1926 $circ->recurring_fine($policy->{recurring_fine});
1927 $circ->duration_rule($duration->name);
1928 $circ->recurring_fine_rule($recurring->name);
1929 $circ->max_fine_rule($max->name);
1930 $circ->max_fine($policy->{max_fine});
1931 $circ->fine_interval($recurring->recurrence_interval);
1932 $circ->renewal_remaining($duration->max_renewals);
1933 $circ->grace_period($policy->{grace_period});
1937 $logger->info("circulator: copy found with an unlimited circ duration");
1938 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1939 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1940 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1941 $circ->renewal_remaining(0);
1942 $circ->grace_period(0);
1945 $circ->target_copy( $copy->id );
1946 $circ->usr( $patron->id );
1947 $circ->circ_lib( $self->circ_lib );
1948 $circ->workstation($self->editor->requestor->wsid)
1949 if defined $self->editor->requestor->wsid;
1951 # renewals maintain a link to the parent circulation
1952 $circ->parent_circ($self->parent_circ);
1954 if( $self->is_renewal ) {
1955 $circ->opac_renewal('t') if $self->opac_renewal;
1956 $circ->phone_renewal('t') if $self->phone_renewal;
1957 $circ->desk_renewal('t') if $self->desk_renewal;
1958 $circ->renewal_remaining($self->renewal_remaining);
1959 $circ->circ_staff($self->editor->requestor->id);
1963 # if the user provided an overiding checkout time,
1964 # (e.g. the checkout really happened several hours ago), then
1965 # we apply that here. Does this need a perm??
1966 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1967 if $self->checkout_time;
1969 # if a patron is renewing, 'requestor' will be the patron
1970 $circ->circ_staff($self->editor->requestor->id);
1971 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1976 sub do_reservation_pickup {
1979 $self->log_me("do_reservation_pickup()");
1981 $self->reservation->pickup_time('now');
1984 $self->reservation->current_resource &&
1985 $U->is_true($self->reservation->target_resource_type->catalog_item)
1987 # We used to try to set $self->copy and $self->patron here,
1988 # but that should already be done.
1990 $self->run_checkout_scripts(1);
1992 my $duration = $self->duration_rule;
1993 my $max = $self->max_fine_rule;
1994 my $recurring = $self->recurring_fines_rule;
1996 if ($duration && $max && $recurring) {
1997 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1999 my $dname = $duration->name;
2000 my $mname = $max->name;
2001 my $rname = $recurring->name;
2003 $logger->debug("circulator: updating reservation ".
2004 "with duration=$dname, maxfine=$mname, recurring=$rname");
2006 $self->reservation->fine_amount($policy->{recurring_fine});
2007 $self->reservation->max_fine($policy->{max_fine});
2008 $self->reservation->fine_interval($recurring->recurrence_interval);
2011 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2012 $self->update_copy();
2015 $self->reservation->fine_amount(
2016 $self->reservation->target_resource_type->fine_amount
2018 $self->reservation->max_fine(
2019 $self->reservation->target_resource_type->max_fine
2021 $self->reservation->fine_interval(
2022 $self->reservation->target_resource_type->fine_interval
2026 $self->update_reservation();
2029 sub do_reservation_return {
2031 my $request = shift;
2033 $self->log_me("do_reservation_return()");
2035 if (not ref $self->reservation) {
2036 my ($reservation, $evt) =
2037 $U->fetch_booking_reservation($self->reservation);
2038 return $self->bail_on_events($evt) if $evt;
2039 $self->reservation($reservation);
2042 $self->generate_fines(1);
2043 $self->reservation->return_time('now');
2044 $self->update_reservation();
2045 $self->reshelve_copy if $self->copy;
2047 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2048 $self->copy( $self->reservation->current_resource->catalog_item );
2052 sub booking_adjusted_due_date {
2054 my $circ = $self->circ;
2055 my $copy = $self->copy;
2057 return undef unless $self->use_booking;
2061 if( $self->due_date ) {
2063 return $self->bail_on_events($self->editor->event)
2064 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2066 $circ->due_date(cleanse_ISO8601($self->due_date));
2070 return unless $copy and $circ->due_date;
2073 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2074 if (@$booking_items) {
2075 my $booking_item = $booking_items->[0];
2076 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2078 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2079 my $shorten_circ_setting = $resource_type->elbow_room ||
2080 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2083 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2084 my $bookings = $booking_ses->request(
2085 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2086 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2088 $booking_ses->disconnect;
2090 my $dt_parser = DateTime::Format::ISO8601->new;
2091 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2093 for my $bid (@$bookings) {
2095 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2097 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2098 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2100 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2101 if ($booking_start < DateTime->now);
2104 if ($U->is_true($stop_circ_setting)) {
2105 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2107 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2108 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2111 # We set the circ duration here only to affect the logic that will
2112 # later (in a DB trigger) mangle the time part of the due date to
2113 # 11:59pm. Having any circ duration that is not a whole number of
2114 # days is enough to prevent the "correction."
2115 my $new_circ_duration = $due_date->epoch - time;
2116 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2117 $circ->duration("$new_circ_duration seconds");
2119 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2123 return $self->bail_on_events($self->editor->event)
2124 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2130 sub apply_modified_due_date {
2132 my $shift_earlier = shift;
2133 my $circ = $self->circ;
2134 my $copy = $self->copy;
2136 if( $self->due_date ) {
2138 return $self->bail_on_events($self->editor->event)
2139 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2141 $circ->due_date(cleanse_ISO8601($self->due_date));
2145 # if the due_date lands on a day when the location is closed
2146 return unless $copy and $circ->due_date;
2148 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2150 # due-date overlap should be determined by the location the item
2151 # is checked out from, not the owning or circ lib of the item
2152 my $org = $self->circ_lib;
2154 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2155 " with an item due date of ".$circ->due_date );
2157 my $dateinfo = $U->storagereq(
2158 'open-ils.storage.actor.org_unit.closed_date.overlap',
2159 $org, $circ->due_date );
2162 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2163 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2165 # XXX make the behavior more dynamic
2166 # for now, we just push the due date to after the close date
2167 if ($shift_earlier) {
2168 $circ->due_date($dateinfo->{start});
2170 $circ->due_date($dateinfo->{end});
2178 sub create_due_date {
2179 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2181 # if there is a raw time component (e.g. from postgres),
2182 # turn it into an interval that interval_to_seconds can parse
2183 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2185 # for now, use the server timezone. TODO: use workstation org timezone
2186 my $due_date = DateTime->now(time_zone => 'local');
2188 # add the circ duration
2189 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2192 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2193 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2194 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2199 # return ISO8601 time with timezone
2200 return $due_date->strftime('%FT%T%z');
2205 sub make_precat_copy {
2207 my $copy = $self->copy;
2210 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2212 $copy->editor($self->editor->requestor->id);
2213 $copy->edit_date('now');
2214 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2215 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2216 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2217 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2218 $self->update_copy();
2222 $logger->info("circulator: Creating a new precataloged ".
2223 "copy in checkout with barcode " . $self->copy_barcode);
2225 $copy = Fieldmapper::asset::copy->new;
2226 $copy->circ_lib($self->circ_lib);
2227 $copy->creator($self->editor->requestor->id);
2228 $copy->editor($self->editor->requestor->id);
2229 $copy->barcode($self->copy_barcode);
2230 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2231 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2232 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2234 $copy->dummy_title($self->dummy_title || "");
2235 $copy->dummy_author($self->dummy_author || "");
2236 $copy->dummy_isbn($self->dummy_isbn || "");
2237 $copy->circ_modifier($self->circ_modifier);
2240 # See if we need to override the circ_lib for the copy with a configured circ_lib
2241 # Setting is shortname of the org unit
2242 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2243 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2245 if($precat_circ_lib) {
2246 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2249 $self->bail_on_events($self->editor->event);
2253 $copy->circ_lib($org->id);
2257 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2259 $self->push_events($self->editor->event);
2263 # this is a little bit of a hack, but we need to
2264 # get the copy into the script runner
2265 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2269 sub checkout_noncat {
2275 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2276 my $count = $self->noncat_count || 1;
2277 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2279 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2283 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2284 $self->editor->requestor->id,
2292 $self->push_events($evt);
2300 # If a copy goes into transit and is then checked in before the transit checkin
2301 # interval has expired, push an event onto the overridable events list.
2302 sub check_transit_checkin_interval {
2305 # only concerned with in-transit items
2306 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2308 # no interval, no problem
2309 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2310 return unless $interval;
2312 # capture the transit so we don't have to fetch it again later during checkin
2314 $self->editor->search_action_transit_copy(
2315 {target_copy => $self->copy->id, dest_recv_time => undef}
2319 # transit from X to X for whatever reason has no min interval
2320 return if $self->transit->source == $self->transit->dest;
2322 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2323 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2324 my $horizon = $t_start->add(seconds => $seconds);
2326 # See if we are still within the transit checkin forbidden range
2327 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2328 if $horizon > DateTime->now;
2331 # Retarget local holds at checkin
2332 sub checkin_retarget {
2334 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2335 return unless $self->is_checkin; # Renewals need not be checked
2336 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2337 return if $self->is_precat; # No holds for precats
2338 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2339 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2340 my $status = $U->copy_status($self->copy->status);
2341 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2342 # Specifically target items that are likely new (by status ID)
2343 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2344 my $location = $self->copy->location;
2345 if(!ref($location)) {
2346 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2347 $self->copy->location($location);
2349 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2351 # Fetch holds for the bib
2352 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2353 $self->editor->authtoken,
2356 capture_time => undef, # No touching captured holds
2357 frozen => 'f', # Don't bother with frozen holds
2358 pickup_lib => $self->circ_lib # Only holds actually here
2361 # Error? Skip the step.
2362 return if exists $result->{"ilsevent"};
2366 foreach my $holdlist (keys %{$result}) {
2367 push @$holds, @{$result->{$holdlist}};
2370 return if scalar(@$holds) == 0; # No holds, no retargeting
2372 # Check for parts on this copy
2373 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2374 my %parts_hash = ();
2375 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2377 # Loop over holds in request-ish order
2378 # Stage 1: Get them into request-ish order
2379 # Also grab type and target for skipping low hanging ones
2380 $result = $self->editor->json_query({
2381 "select" => { "ahr" => ["id", "hold_type", "target"] },
2382 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2383 "where" => { "id" => $holds },
2385 { "class" => "pgt", "field" => "hold_priority"},
2386 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2387 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2388 { "class" => "ahr", "field" => "request_time"}
2393 if (ref $result eq "ARRAY" and scalar @$result) {
2394 foreach (@{$result}) {
2395 # Copy level, but not this copy?
2396 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2397 and $_->{target} != $self->copy->id);
2398 # Volume level, but not this volume?
2399 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2400 if(@$parts) { # We have parts?
2402 next if ($_->{hold_type} eq 'T');
2403 # Skip part holds for parts not on this copy
2404 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2406 # No parts, no part holds
2407 next if ($_->{hold_type} eq 'P');
2409 # So much for easy stuff, attempt a retarget!
2410 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2411 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2412 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2420 $self->log_me("do_checkin()");
2422 return $self->bail_on_events(
2423 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2426 $self->check_transit_checkin_interval;
2427 $self->checkin_retarget;
2429 # the renew code and mk_env should have already found our circulation object
2430 unless( $self->circ ) {
2432 my $circs = $self->editor->search_action_circulation(
2433 { target_copy => $self->copy->id, checkin_time => undef });
2435 $self->circ($$circs[0]);
2437 # for now, just warn if there are multiple open circs on a copy
2438 $logger->warn("circulator: we have ".scalar(@$circs).
2439 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2442 # run the fine generator against this circ, if this circ is there
2443 $self->generate_fines_start if $self->circ;
2445 if( $self->checkin_check_holds_shelf() ) {
2446 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2447 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2448 if($self->fake_hold_dest) {
2449 $self->hold->pickup_lib($self->circ_lib);
2451 $self->checkin_flesh_events;
2455 unless( $self->is_renewal ) {
2456 return $self->bail_on_events($self->editor->event)
2457 unless $self->editor->allowed('COPY_CHECKIN');
2460 $self->push_events($self->check_copy_alert());
2461 $self->push_events($self->check_checkin_copy_status());
2463 # if the circ is marked as 'claims returned', add the event to the list
2464 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2465 if ($self->circ and $self->circ->stop_fines
2466 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2468 $self->check_circ_deposit();
2470 # handle the overridable events
2471 $self->override_events unless $self->is_renewal;
2472 return if $self->bail_out;
2474 if( $self->copy and !$self->transit ) {
2476 $self->editor->search_action_transit_copy(
2477 { target_copy => $self->copy->id, dest_recv_time => undef }
2483 $self->generate_fines_finish;
2484 $self->checkin_handle_circ;
2485 return if $self->bail_out;
2486 $self->checkin_changed(1);
2488 } elsif( $self->transit ) {
2489 my $hold_transit = $self->process_received_transit;
2490 $self->checkin_changed(1);
2492 if( $self->bail_out ) {
2493 $self->checkin_flesh_events;
2497 if( my $e = $self->check_checkin_copy_status() ) {
2498 # If the original copy status is special, alert the caller
2499 my $ev = $self->events;
2500 $self->events([$e]);
2501 $self->override_events;
2502 return if $self->bail_out;
2506 if( $hold_transit or
2507 $U->copy_status($self->copy->status)->id
2508 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2511 if( $hold_transit ) {
2512 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2514 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2519 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2521 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2522 $self->reshelve_copy(1);
2523 $self->cancelled_hold_transit(1);
2524 $self->notify_hold(0); # don't notify for cancelled holds
2525 $self->fake_hold_dest(0);
2526 return if $self->bail_out;
2528 } elsif ($hold and $hold->hold_type eq 'R') {
2530 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2531 $self->notify_hold(0); # No need to notify
2532 $self->fake_hold_dest(0);
2533 $self->noop(1); # Don't try and capture for other holds/transits now
2534 $self->update_copy();
2535 $hold->fulfillment_time('now');
2536 $self->bail_on_events($self->editor->event)
2537 unless $self->editor->update_action_hold_request($hold);
2541 # hold transited to correct location
2542 if($self->fake_hold_dest) {
2543 $hold->pickup_lib($self->circ_lib);
2545 $self->checkin_flesh_events;
2550 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2552 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2553 " that is in-transit, but there is no transit.. repairing");
2554 $self->reshelve_copy(1);
2555 return if $self->bail_out;
2558 if( $self->is_renewal ) {
2559 $self->finish_fines_and_voiding;
2560 return if $self->bail_out;
2561 $self->push_events(OpenILS::Event->new('SUCCESS'));
2565 # ------------------------------------------------------------------------------
2566 # Circulations and transits are now closed where necessary. Now go on to see if
2567 # this copy can fulfill a hold or needs to be routed to a different location
2568 # ------------------------------------------------------------------------------
2570 my $needed_for_something = 0; # formerly "needed_for_hold"
2572 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2574 if (!$self->remote_hold) {
2575 if ($self->use_booking) {
2576 my $potential_hold = $self->hold_capture_is_possible;
2577 my $potential_reservation = $self->reservation_capture_is_possible;
2579 if ($potential_hold and $potential_reservation) {
2580 $logger->info("circulator: item could fulfill either hold or reservation");
2581 $self->push_events(new OpenILS::Event(
2582 "HOLD_RESERVATION_CONFLICT",
2583 "hold" => $potential_hold,
2584 "reservation" => $potential_reservation
2586 return if $self->bail_out;
2587 } elsif ($potential_hold) {
2588 $needed_for_something =
2589 $self->attempt_checkin_hold_capture;
2590 } elsif ($potential_reservation) {
2591 $needed_for_something =
2592 $self->attempt_checkin_reservation_capture;
2595 $needed_for_something = $self->attempt_checkin_hold_capture;
2598 return if $self->bail_out;
2600 unless($needed_for_something) {
2601 my $circ_lib = (ref $self->copy->circ_lib) ?
2602 $self->copy->circ_lib->id : $self->copy->circ_lib;
2604 if( $self->remote_hold ) {
2605 $circ_lib = $self->remote_hold->pickup_lib;
2606 $logger->warn("circulator: Copy ".$self->copy->barcode.
2607 " is on a remote hold's shelf, sending to $circ_lib");
2610 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2612 my $suppress_transit = 0;
2614 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2615 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2616 if($suppress_transit_source && $suppress_transit_source->{value}) {
2617 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2618 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2619 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2620 $suppress_transit = 1;
2625 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2626 # copy is where it needs to be, either for hold or reshelving
2628 $self->checkin_handle_precat();
2629 return if $self->bail_out;
2632 # copy needs to transit "home", or stick here if it's a floating copy
2634 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2635 $self->checkin_changed(1);
2636 $self->copy->circ_lib( $self->circ_lib );
2639 my $bc = $self->copy->barcode;
2640 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2641 $self->checkin_build_copy_transit($circ_lib);
2642 return if $self->bail_out;
2643 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2647 } else { # no-op checkin
2648 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2649 $self->checkin_changed(1);
2650 $self->copy->circ_lib( $self->circ_lib );
2655 if($self->claims_never_checked_out and
2656 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2658 # the item was not supposed to be checked out to the user and should now be marked as missing
2659 $self->copy->status(OILS_COPY_STATUS_MISSING);
2663 $self->reshelve_copy unless $needed_for_something;
2666 return if $self->bail_out;
2668 unless($self->checkin_changed) {
2670 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2671 my $stat = $U->copy_status($self->copy->status)->id;
2673 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2674 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2675 $self->bail_out(1); # no need to commit anything
2679 $self->push_events(OpenILS::Event->new('SUCCESS'))
2680 unless @{$self->events};
2683 $self->finish_fines_and_voiding;
2685 OpenILS::Utils::Penalty->calculate_penalties(
2686 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2688 $self->checkin_flesh_events;
2692 sub finish_fines_and_voiding {
2694 return unless $self->circ;
2696 # gather any updates to the circ after fine generation, if there was a circ
2697 $self->generate_fines_finish;
2699 return unless $self->backdate or $self->void_overdues;
2701 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2702 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2704 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2705 $self->editor, $self->circ, $self->backdate, $note);
2707 return $self->bail_on_events($evt) if $evt;
2709 # make sure the circ isn't closed if we just voided some fines
2710 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2711 return $self->bail_on_events($evt) if $evt;
2717 # if a deposit was payed for this item, push the event
2718 sub check_circ_deposit {
2720 return unless $self->circ;
2721 my $deposit = $self->editor->search_money_billing(
2723 xact => $self->circ->id,
2725 }, {idlist => 1})->[0];
2727 $self->push_events(OpenILS::Event->new(
2728 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2733 my $force = $self->force || shift;
2734 my $copy = $self->copy;
2736 my $stat = $U->copy_status($copy->status)->id;
2739 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2740 $stat != OILS_COPY_STATUS_CATALOGING and
2741 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2742 $stat != OILS_COPY_STATUS_RESHELVING )) {
2744 $copy->status( OILS_COPY_STATUS_RESHELVING );
2746 $self->checkin_changed(1);
2751 # Returns true if the item is at the current location
2752 # because it was transited there for a hold and the
2753 # hold has not been fulfilled
2754 sub checkin_check_holds_shelf {
2756 return 0 unless $self->copy;
2759 $U->copy_status($self->copy->status)->id ==
2760 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2762 # Attempt to clear shelf expired holds for this copy
2763 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2764 if($self->clear_expired);
2766 # find the hold that put us on the holds shelf
2767 my $holds = $self->editor->search_action_hold_request(
2769 current_copy => $self->copy->id,
2770 capture_time => { '!=' => undef },
2771 fulfillment_time => undef,
2772 cancel_time => undef,
2777 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2778 $self->reshelve_copy(1);
2782 my $hold = $$holds[0];
2784 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2785 $hold->id. "] for copy ".$self->copy->barcode);
2787 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2788 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2789 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2790 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2791 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2792 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2793 $self->fake_hold_dest(1);
2799 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2800 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2804 $logger->info("circulator: hold is not for here..");
2805 $self->remote_hold($hold);
2810 sub checkin_handle_precat {
2812 my $copy = $self->copy;
2814 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2815 $copy->status(OILS_COPY_STATUS_CATALOGING);
2816 $self->update_copy();
2817 $self->checkin_changed(1);
2818 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2823 sub checkin_build_copy_transit {
2826 my $copy = $self->copy;
2827 my $transit = Fieldmapper::action::transit_copy->new;
2829 # if we are transiting an item to the shelf shelf, it's a hold transit
2830 if (my $hold = $self->remote_hold) {
2831 $transit = Fieldmapper::action::hold_transit_copy->new;
2832 $transit->hold($hold->id);
2834 # the item is going into transit, remove any shelf-iness
2835 if ($hold->current_shelf_lib or $hold->shelf_time) {
2836 $hold->clear_current_shelf_lib;
2837 $hold->clear_shelf_time;
2838 return $self->bail_on_events($self->editor->event)
2839 unless $self->editor->update_action_hold_request($hold);
2843 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2844 $logger->info("circulator: transiting copy to $dest");
2846 $transit->source($self->circ_lib);
2847 $transit->dest($dest);
2848 $transit->target_copy($copy->id);
2849 $transit->source_send_time('now');
2850 $transit->copy_status( $U->copy_status($copy->status)->id );
2852 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2854 if ($self->remote_hold) {
2855 return $self->bail_on_events($self->editor->event)
2856 unless $self->editor->create_action_hold_transit_copy($transit);
2858 return $self->bail_on_events($self->editor->event)
2859 unless $self->editor->create_action_transit_copy($transit);
2862 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2864 $self->checkin_changed(1);
2868 sub hold_capture_is_possible {
2870 my $copy = $self->copy;
2872 # we've been explicitly told not to capture any holds
2873 return 0 if $self->capture eq 'nocapture';
2875 # See if this copy can fulfill any holds
2876 my $hold = $holdcode->find_nearest_permitted_hold(
2877 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2879 return undef if ref $hold eq "HASH" and
2880 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2884 sub reservation_capture_is_possible {
2886 my $copy = $self->copy;
2888 # we've been explicitly told not to capture any holds
2889 return 0 if $self->capture eq 'nocapture';
2891 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2892 my $resv = $booking_ses->request(
2893 "open-ils.booking.reservations.could_capture",
2894 $self->editor->authtoken, $copy->barcode
2896 $booking_ses->disconnect;
2897 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2898 $self->push_events($resv);
2904 # returns true if the item was used (or may potentially be used
2905 # in subsequent calls) to capture a hold.
2906 sub attempt_checkin_hold_capture {
2908 my $copy = $self->copy;
2910 # we've been explicitly told not to capture any holds
2911 return 0 if $self->capture eq 'nocapture';
2913 # See if this copy can fulfill any holds
2914 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2915 $self->editor, $copy, $self->editor->requestor );
2918 $logger->debug("circulator: no potential permitted".
2919 "holds found for copy ".$copy->barcode);
2923 if($self->capture ne 'capture') {
2924 # see if this item is in a hold-capture-delay location
2925 my $location = $self->copy->location;
2926 if(!ref($location)) {
2927 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2928 $self->copy->location($location);
2930 if($U->is_true($location->hold_verify)) {
2931 $self->bail_on_events(
2932 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2937 $self->retarget($retarget);
2939 my $suppress_transit = 0;
2940 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2941 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2942 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2943 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2944 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2945 $suppress_transit = 1;
2946 $self->hold->pickup_lib($self->circ_lib);
2951 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2953 $hold->current_copy($copy->id);
2954 $hold->capture_time('now');
2955 $self->put_hold_on_shelf($hold)
2956 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2958 # prevent DB errors caused by fetching
2959 # holds from storage, and updating through cstore
2960 $hold->clear_fulfillment_time;
2961 $hold->clear_fulfillment_staff;
2962 $hold->clear_fulfillment_lib;
2963 $hold->clear_expire_time;
2964 $hold->clear_cancel_time;
2965 $hold->clear_prev_check_time unless $hold->prev_check_time;
2967 $self->bail_on_events($self->editor->event)
2968 unless $self->editor->update_action_hold_request($hold);
2970 $self->checkin_changed(1);
2972 return 0 if $self->bail_out;
2974 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2976 if ($hold->hold_type eq 'R') {
2977 $copy->status(OILS_COPY_STATUS_CATALOGING);
2978 $hold->fulfillment_time('now');
2979 $self->noop(1); # Block other transit/hold checks
2980 $self->bail_on_events($self->editor->event)
2981 unless $self->editor->update_action_hold_request($hold);
2983 # This hold was captured in the correct location
2984 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2985 $self->push_events(OpenILS::Event->new('SUCCESS'));
2987 #$self->do_hold_notify($hold->id);
2988 $self->notify_hold($hold->id);
2993 # Hold needs to be picked up elsewhere. Build a hold
2994 # transit and route the item.
2995 $self->checkin_build_hold_transit();
2996 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2997 return 0 if $self->bail_out;
2998 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3001 # make sure we save the copy status
3003 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3007 sub attempt_checkin_reservation_capture {
3009 my $copy = $self->copy;
3011 # we've been explicitly told not to capture any holds
3012 return 0 if $self->capture eq 'nocapture';
3014 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3015 my $evt = $booking_ses->request(
3016 "open-ils.booking.resources.capture_for_reservation",
3017 $self->editor->authtoken,
3019 1 # don't update copy - we probably have it locked
3021 $booking_ses->disconnect;
3023 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3025 "open-ils.booking.resources.capture_for_reservation " .
3026 "didn't return an event!"
3030 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3031 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3033 # not-transferable is an error event we'll pass on the user
3034 $logger->warn("reservation capture attempted against non-transferable item");
3035 $self->push_events($evt);
3037 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3038 # Re-retrieve copy as reservation capture may have changed
3039 # its status and whatnot.
3041 "circulator: booking capture win on copy " . $self->copy->id
3043 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3045 "circulator: changing copy " . $self->copy->id .
3046 "'s status from " . $self->copy->status . " to " .
3049 $self->copy->status($new_copy_status);
3052 $self->reservation($evt->{"payload"}->{"reservation"});
3054 if (exists $evt->{"payload"}->{"transit"}) {
3058 "org" => $evt->{"payload"}->{"transit"}->dest
3062 $self->checkin_changed(1);
3066 # other results are treated as "nothing to capture"
3070 sub do_hold_notify {
3071 my( $self, $holdid ) = @_;
3073 my $e = new_editor(xact => 1);
3074 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3076 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3077 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3079 $logger->info("circulator: running delayed hold notify process");
3081 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3082 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3084 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3085 hold_id => $holdid, requestor => $self->editor->requestor);
3087 $logger->debug("circulator: built hold notifier");
3089 if(!$notifier->event) {
3091 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3093 my $stat = $notifier->send_email_notify;
3094 if( $stat == '1' ) {
3095 $logger->info("circulator: hold notify succeeded for hold $holdid");
3099 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3102 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3106 sub retarget_holds {
3108 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3109 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3110 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3111 # no reason to wait for the return value
3115 sub checkin_build_hold_transit {
3118 my $copy = $self->copy;
3119 my $hold = $self->hold;
3120 my $trans = Fieldmapper::action::hold_transit_copy->new;
3122 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3124 $trans->hold($hold->id);
3125 $trans->source($self->circ_lib);
3126 $trans->dest($hold->pickup_lib);
3127 $trans->source_send_time("now");
3128 $trans->target_copy($copy->id);
3130 # when the copy gets to its destination, it will recover
3131 # this status - put it onto the holds shelf
3132 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3134 return $self->bail_on_events($self->editor->event)
3135 unless $self->editor->create_action_hold_transit_copy($trans);
3140 sub process_received_transit {
3142 my $copy = $self->copy;
3143 my $copyid = $self->copy->id;
3145 my $status_name = $U->copy_status($copy->status)->name;
3146 $logger->debug("circulator: attempting transit receive on ".
3147 "copy $copyid. Copy status is $status_name");
3149 my $transit = $self->transit;
3151 # Check if we are in a transit suppress range
3152 my $suppress_transit = 0;
3153 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3154 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3155 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3156 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3157 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3158 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3159 $suppress_transit = 1;
3160 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3164 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3165 # - this item is in-transit to a different location
3166 # - Or we are capturing holds as transits, so why create a new transit?
3168 my $tid = $transit->id;
3169 my $loc = $self->circ_lib;
3170 my $dest = $transit->dest;
3172 $logger->info("circulator: Fowarding transit on copy which is destined ".
3173 "for a different location. transit=$tid, copy=$copyid, current ".
3174 "location=$loc, destination location=$dest");
3176 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3178 # grab the associated hold object if available
3179 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3180 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3182 return $self->bail_on_events($evt);
3185 # The transit is received, set the receive time
3186 $transit->dest_recv_time('now');
3187 $self->bail_on_events($self->editor->event)
3188 unless $self->editor->update_action_transit_copy($transit);
3190 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3192 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3193 $copy->status( $transit->copy_status );
3194 $self->update_copy();
3195 return if $self->bail_out;
3199 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3201 # hold has arrived at destination, set shelf time
3202 $self->put_hold_on_shelf($hold);
3203 $self->bail_on_events($self->editor->event)
3204 unless $self->editor->update_action_hold_request($hold);
3205 return if $self->bail_out;
3207 $self->notify_hold($hold_transit->hold);
3212 OpenILS::Event->new(
3215 payload => { transit => $transit, holdtransit => $hold_transit } ));
3217 return $hold_transit;
3221 # ------------------------------------------------------------------
3222 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3223 # ------------------------------------------------------------------
3224 sub put_hold_on_shelf {
3225 my($self, $hold) = @_;
3226 $hold->shelf_time('now');
3227 $hold->current_shelf_lib($self->circ_lib);
3228 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3234 sub generate_fines {
3236 my $reservation = shift;
3238 $self->generate_fines_start($reservation);
3239 $self->generate_fines_finish($reservation);
3244 sub generate_fines_start {
3246 my $reservation = shift;
3247 my $dt_parser = DateTime::Format::ISO8601->new;
3249 my $obj = $reservation ? $self->reservation : $self->circ;
3251 # If we have a grace period
3252 if($obj->can('grace_period')) {
3253 # Parse out the due date
3254 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3255 # Add the grace period to the due date
3256 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3257 # Don't generate fines on circs still in grace period
3258 return undef if ($due_date > DateTime->now);
3261 if (!exists($self->{_gen_fines_req})) {
3262 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3264 'open-ils.storage.action.circulation.overdue.generate_fines',
3272 sub generate_fines_finish {
3274 my $reservation = shift;
3276 return undef unless $self->{_gen_fines_req};
3278 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3280 $self->{_gen_fines_req}->wait_complete;
3281 delete($self->{_gen_fines_req});
3283 # refresh the circ in case the fine generator set the stop_fines field
3284 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3285 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3290 sub checkin_handle_circ {
3292 my $circ = $self->circ;
3293 my $copy = $self->copy;
3297 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3299 # backdate the circ if necessary
3300 if($self->backdate) {
3301 my $evt = $self->checkin_handle_backdate;
3302 return $self->bail_on_events($evt) if $evt;
3305 if(!$circ->stop_fines) {
3306 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3307 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3308 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3309 $circ->stop_fines_time('now');
3310 $circ->stop_fines_time($self->backdate) if $self->backdate;
3313 # Set the checkin vars since we have the item
3314 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3316 # capture the true scan time for back-dated checkins
3317 $circ->checkin_scan_time('now');
3319 $circ->checkin_staff($self->editor->requestor->id);
3320 $circ->checkin_lib($self->circ_lib);
3321 $circ->checkin_workstation($self->editor->requestor->wsid);
3323 my $circ_lib = (ref $self->copy->circ_lib) ?
3324 $self->copy->circ_lib->id : $self->copy->circ_lib;
3325 my $stat = $U->copy_status($self->copy->status)->id;
3327 if ($stat == OILS_COPY_STATUS_LOST) {
3328 # we will now handle lost fines, but the copy will retain its 'lost'
3329 # status if it needs to transit home unless lost_immediately_available
3332 # if we decide to also delay fine handling until the item arrives home,
3333 # we will need to call lost fine handling code both when checking items
3334 # in and also when receiving transits
3335 $self->checkin_handle_lost($circ_lib);
3336 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3337 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3339 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3344 # see if there are any fines owed on this circ. if not, close it
3345 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3346 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3348 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3350 return $self->bail_on_events($self->editor->event)
3351 unless $self->editor->update_action_circulation($circ);
3357 # ------------------------------------------------------------------
3358 # See if we need to void billings for lost checkin
3359 # ------------------------------------------------------------------
3360 sub checkin_handle_lost {
3362 my $circ_lib = shift;
3363 my $circ = $self->circ;
3365 my $max_return = $U->ou_ancestor_setting_value(
3366 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3371 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3372 $tm[5] -= 1 if $tm[5] > 0;
3373 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3375 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3376 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3378 $max_return = 0 if $today < $last_chance;
3381 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3383 my $void_lost = $U->ou_ancestor_setting_value(
3384 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3385 my $void_lost_fee = $U->ou_ancestor_setting_value(
3386 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3387 my $restore_od = $U->ou_ancestor_setting_value(
3388 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3389 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3390 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3392 $self->checkin_handle_lost_now_found(3) if $void_lost;
3393 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3394 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3397 if ($circ_lib != $self->circ_lib) {
3398 # if the item is not home, check to see if we want to retain the lost
3399 # status at this point in the process
3400 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3402 if ($immediately_available) {
3403 # lost item status does not need to be retained, so give it a
3404 # reshelving status as if it were a normal checkin
3405 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3408 $logger->info("circulator: not updating copy status on checkin because copy is lost");
3411 # lost item is home and processed, treat like a normal checkin from
3413 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3419 sub checkin_handle_backdate {
3422 # ------------------------------------------------------------------
3423 # clean up the backdate for date comparison
3424 # XXX We are currently taking the due-time from the original due-date,
3425 # not the input. Do we need to do this? This certainly interferes with
3426 # backdating of hourly checkouts, but that is likely a very rare case.
3427 # ------------------------------------------------------------------
3428 my $bd = cleanse_ISO8601($self->backdate);
3429 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3430 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3431 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3433 $self->backdate($bd);
3438 sub check_checkin_copy_status {
3440 my $copy = $self->copy;
3442 my $status = $U->copy_status($copy->status)->id;
3445 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3446 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3447 $status == OILS_COPY_STATUS_IN_PROCESS ||
3448 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3449 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3450 $status == OILS_COPY_STATUS_CATALOGING ||
3451 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3452 $status == OILS_COPY_STATUS_RESHELVING );
3454 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3455 if( $status == OILS_COPY_STATUS_LOST );
3457 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3458 if( $status == OILS_COPY_STATUS_MISSING );
3460 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3465 # --------------------------------------------------------------------------
3466 # On checkin, we need to return as many relevant objects as we can
3467 # --------------------------------------------------------------------------
3468 sub checkin_flesh_events {
3471 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3472 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3473 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3476 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3479 if($self->hold and !$self->hold->cancel_time) {
3480 $hold = $self->hold;
3481 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3485 # if we checked in a circulation, flesh the billing summary data
3486 $self->circ->billable_transaction(
3487 $self->editor->retrieve_money_billable_transaction([
3489 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3495 # flesh some patron fields before returning
3497 $self->editor->retrieve_actor_user([
3502 au => ['card', 'billing_address', 'mailing_address']
3509 for my $evt (@{$self->events}) {
3512 $payload->{copy} = $U->unflesh_copy($self->copy);
3513 $payload->{volume} = $self->volume;
3514 $payload->{record} = $record,
3515 $payload->{circ} = $self->circ;
3516 $payload->{transit} = $self->transit;
3517 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3518 $payload->{hold} = $hold;
3519 $payload->{patron} = $self->patron;
3520 $payload->{reservation} = $self->reservation
3521 unless (not $self->reservation or $self->reservation->cancel_time);
3523 $evt->{payload} = $payload;
3528 my( $self, $msg ) = @_;
3529 my $bc = ($self->copy) ? $self->copy->barcode :
3532 my $usr = ($self->patron) ? $self->patron->id : "";
3533 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3534 ", recipient=$usr, copy=$bc");
3540 $self->log_me("do_renew()");
3542 # Make sure there is an open circ to renew that is not
3543 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3544 my $usrid = $self->patron->id if $self->patron;
3545 my $circ = $self->editor->search_action_circulation({
3546 target_copy => $self->copy->id,
3547 xact_finish => undef,
3548 checkin_time => undef,
3549 ($usrid ? (usr => $usrid) : ()),
3551 {stop_fines => undef},
3552 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3556 return $self->bail_on_events($self->editor->event) unless $circ;
3558 # A user is not allowed to renew another user's items without permission
3559 unless( $circ->usr eq $self->editor->requestor->id ) {
3560 return $self->bail_on_events($self->editor->events)
3561 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3564 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3565 if $circ->renewal_remaining < 1;
3567 # -----------------------------------------------------------------
3569 $self->parent_circ($circ->id);
3570 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3573 # Opac renewal - re-use circ library from original circ (unless told not to)
3574 if($self->opac_renewal) {
3575 unless(defined($opac_renewal_use_circ_lib)) {
3576 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3577 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3578 $opac_renewal_use_circ_lib = 1;
3581 $opac_renewal_use_circ_lib = 0;
3584 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3587 # Run the fine generator against the old circ
3588 $self->generate_fines_start;
3590 $self->run_renew_permit;
3593 $self->do_checkin();
3594 return if $self->bail_out;
3596 unless( $self->permit_override ) {
3598 return if $self->bail_out;
3599 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3600 $self->remove_event('ITEM_NOT_CATALOGED');
3603 $self->override_events;
3604 return if $self->bail_out;
3607 $self->do_checkout();
3612 my( $self, $evt ) = @_;
3613 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3614 $logger->debug("circulator: removing event from list: $evt");
3615 my @events = @{$self->events};
3616 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3621 my( $self, $evt ) = @_;
3622 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3623 return grep { $_->{textcode} eq $evt } @{$self->events};
3628 sub run_renew_permit {
3631 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3632 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3633 $self->editor, $self->copy, $self->editor->requestor, 1
3635 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3638 if(!$self->legacy_script_support) {
3639 my $results = $self->run_indb_circ_test;
3640 $self->push_events($self->matrix_test_result_events)
3641 unless $self->circ_test_success;
3644 my $runner = $self->script_runner;
3646 $runner->load($self->circ_permit_renew);
3647 my $result = $runner->run or
3648 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3649 if ($result->{"events"}) {
3651 map { new OpenILS::Event($_) } @{$result->{"events"}}
3654 "circulator: circ_permit_renew for user " .
3655 $self->patron->id . " returned " .
3656 scalar(@{$result->{"events"}}) . " event(s)"
3660 $self->mk_script_runner;
3663 $logger->debug("circulator: re-creating script runner to be safe");
3667 # XXX: The primary mechanism for storing circ history is now handled
3668 # by tracking real circulation objects instead of bibs in a bucket.
3669 # However, this code is disabled by default and could be useful
3670 # some day, so may as well leave it for now.
3671 sub append_reading_list {
3675 $self->is_checkout and
3681 # verify history is globally enabled and uses the bucket mechanism
3682 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3683 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3685 return undef unless $htype and $htype eq 'bucket';
3687 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3689 # verify the patron wants to retain the hisory
3690 my $setting = $e->search_actor_user_setting(
3691 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3693 unless($setting and $setting->value) {
3698 my $bkt = $e->search_container_copy_bucket(
3699 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3704 # find the next item position
3705 my $last_item = $e->search_container_copy_bucket_item(
3706 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3707 $pos = $last_item->pos + 1 if $last_item;
3710 # create the history bucket if necessary
3711 $bkt = Fieldmapper::container::copy_bucket->new;
3712 $bkt->owner($self->patron->id);
3714 $bkt->btype('circ_history');
3716 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3719 my $item = Fieldmapper::container::copy_bucket_item->new;
3721 $item->bucket($bkt->id);
3722 $item->target_copy($self->copy->id);
3725 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3732 sub make_trigger_events {
3734 return unless $self->circ;
3735 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3736 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3737 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3742 sub checkin_handle_lost_now_found {
3743 my ($self, $bill_type) = @_;
3745 # ------------------------------------------------------------------
3746 # remove charge from patron's account if lost item is returned
3747 # ------------------------------------------------------------------
3749 my $bills = $self->editor->search_money_billing(
3751 xact => $self->circ->id,
3756 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3757 for my $bill (@$bills) {
3758 if( !$U->is_true($bill->voided) ) {
3759 $logger->info("lost item returned - voiding bill ".$bill->id);
3761 $bill->void_time('now');
3762 $bill->voider($self->editor->requestor->id);
3763 my $note = ($bill->note) ? $bill->note . "\n" : '';
3764 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3766 $self->bail_on_events($self->editor->event)
3767 unless $self->editor->update_money_billing($bill);
3772 sub checkin_handle_lost_now_found_restore_od {
3774 my $circ_lib = shift;
3776 # ------------------------------------------------------------------
3777 # restore those overdue charges voided when item was set to lost
3778 # ------------------------------------------------------------------
3780 my $ods = $self->editor->search_money_billing(
3782 xact => $self->circ->id,
3787 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3788 for my $bill (@$ods) {
3789 if( $U->is_true($bill->voided) ) {
3790 $logger->info("lost item returned - restoring overdue ".$bill->id);
3792 $bill->clear_void_time;
3793 $bill->voider($self->editor->requestor->id);
3794 my $note = ($bill->note) ? $bill->note . "\n" : '';
3795 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3797 $self->bail_on_events($self->editor->event)
3798 unless $self->editor->update_money_billing($bill);
3803 # ------------------------------------------------------------------
3804 # Lost-then-found item checked in. This sub generates new overdue
3805 # fines, beyond the point of any existing and possibly voided
3806 # overdue fines, up to the point of final checkin time (or max fine
3808 # ------------------------------------------------------------------
3809 sub generate_lost_overdue_fines {
3811 my $circ = $self->circ;
3812 my $e = $self->editor;
3814 # Re-open the transaction so the fine generator can see it
3815 if($circ->xact_finish or $circ->stop_fines) {
3817 $circ->clear_xact_finish;
3818 $circ->clear_stop_fines;
3819 $circ->clear_stop_fines_time;
3820 $e->update_action_circulation($circ) or return $e->die_event;
3824 $e->xact_begin; # generate_fines expects an in-xact editor
3825 $self->generate_fines;
3826 $circ = $self->circ; # generate fines re-fetches the circ
3830 # Re-close the transaction if no money is owed
3831 my ($obt) = $U->fetch_mbts($circ->id, $e);
3832 if ($obt and $obt->balance_owed == 0) {
3833 $circ->xact_finish('now');
3837 # Set stop fines if the fine generator didn't have to
3838 unless($circ->stop_fines) {
3839 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3840 $circ->stop_fines_time('now');
3844 # update the event data sent to the caller within the transaction
3845 $self->checkin_flesh_events;
3848 $e->update_action_circulation($circ) or return $e->die_event;