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;
18 my $desk_renewal_use_circ_lib;
20 sub determine_booking_status {
21 unless (defined $booking_status) {
22 my $ses = create OpenSRF::AppSession("router");
23 $booking_status = grep {$_ eq "open-ils.booking"} @{
24 $ses->request("opensrf.router.info.class.list")->gather(1)
27 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
30 return $booking_status;
36 flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
42 my $conf = OpenSRF::Utils::SettingsClient->new;
43 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
45 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
46 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
48 my $lb = $conf->config_value( @pfx2, 'script_path' );
49 $lb = [ $lb ] unless ref($lb);
52 return unless $legacy_script_support;
54 my @pfx = ( @pfx2, "scripts" );
55 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
56 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
57 my $d = $conf->config_value( @pfx, 'circ_duration' );
58 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
59 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
60 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
62 $logger->error( "Missing circ script(s)" )
63 unless( $p and $c and $d and $f and $m and $pr );
65 $scripts{circ_permit_patron} = $p;
66 $scripts{circ_permit_copy} = $c;
67 $scripts{circ_duration} = $d;
68 $scripts{circ_recurring_fines} = $f;
69 $scripts{circ_max_fines} = $m;
70 $scripts{circ_permit_renew} = $pr;
73 "circulator: Loaded rules scripts for circ: " .
74 "circ permit patron = $p, ".
75 "circ permit copy = $c, ".
76 "circ duration = $d, ".
77 "circ recurring fines = $f, " .
78 "circ max fines = $m, ".
79 "circ renew permit = $pr. ".
81 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
85 __PACKAGE__->register_method(
86 method => "run_method",
87 api_name => "open-ils.circ.checkout.permit",
89 Determines if the given checkout can occur
90 @param authtoken The login session key
91 @param params A trailing hash of named params including
92 barcode : The copy barcode,
93 patron : The patron the checkout is occurring for,
94 renew : true or false - whether or not this is a renewal
95 @return The event that occurred during the permit check.
99 __PACKAGE__->register_method (
100 method => 'run_method',
101 api_name => 'open-ils.circ.checkout.permit.override',
102 signature => q/@see open-ils.circ.checkout.permit/,
106 __PACKAGE__->register_method(
107 method => "run_method",
108 api_name => "open-ils.circ.checkout",
111 @param authtoken The login session key
112 @param params A named hash of params including:
114 barcode If no copy is provided, the copy is retrieved via barcode
115 copyid If no copy or barcode is provide, the copy id will be use
116 patron The patron's id
117 noncat True if this is a circulation for a non-cataloted item
118 noncat_type The non-cataloged type id
119 noncat_circ_lib The location for the noncat circ.
120 precat The item has yet to be cataloged
121 dummy_title The temporary title of the pre-cataloded item
122 dummy_author The temporary authr of the pre-cataloded item
123 Default is the home org of the staff member
124 @return The SUCCESS event on success, any other event depending on the error
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.checkin",
132 Generic super-method for handling all copies
133 @param authtoken The login session key
134 @param params Hash of named parameters including:
135 barcode - The copy barcode
136 force - If true, copies in bad statuses will be checked in and give good statuses
137 noop - don't capture holds or put items into transit
138 void_overdues - void all overdues for the circulation (aka amnesty)
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkin.override",
146 signature => q/@see open-ils.circ.checkin/
149 __PACKAGE__->register_method(
150 method => "run_method",
151 api_name => "open-ils.circ.renew.override",
152 signature => q/@see open-ils.circ.renew/,
156 __PACKAGE__->register_method(
157 method => "run_method",
158 api_name => "open-ils.circ.renew",
159 notes => <<" NOTES");
160 PARAMS( authtoken, circ => circ_id );
161 open-ils.circ.renew(login_session, circ_object);
162 Renews the provided circulation. login_session is the requestor of the
163 renewal and if the logged in user is not the same as circ->usr, then
164 the logged in user must have RENEW_CIRC permissions.
167 __PACKAGE__->register_method(
168 method => "run_method",
169 api_name => "open-ils.circ.checkout.full"
171 __PACKAGE__->register_method(
172 method => "run_method",
173 api_name => "open-ils.circ.checkout.full.override"
175 __PACKAGE__->register_method(
176 method => "run_method",
177 api_name => "open-ils.circ.reservation.pickup"
179 __PACKAGE__->register_method(
180 method => "run_method",
181 api_name => "open-ils.circ.reservation.return"
183 __PACKAGE__->register_method(
184 method => "run_method",
185 api_name => "open-ils.circ.reservation.return.override"
187 __PACKAGE__->register_method(
188 method => "run_method",
189 api_name => "open-ils.circ.checkout.inspect",
190 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
195 my( $self, $conn, $auth, $args ) = @_;
196 translate_legacy_args($args);
197 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
198 my $api = $self->api_name;
201 OpenILS::Application::Circ::Circulator->new($auth, %$args);
203 return circ_events($circulator) if $circulator->bail_out;
205 $circulator->use_booking(determine_booking_status());
207 # --------------------------------------------------------------------------
208 # First, check for a booking transit, as the barcode may not be a copy
209 # barcode, but a resource barcode, and nothing else in here will work
210 # --------------------------------------------------------------------------
212 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
213 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
214 if (@$resources) { # yes!
216 my $res_id_list = [ map { $_->id } @$resources ];
217 my $transit = $circulator->editor->search_action_reservation_transit_copy(
219 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
220 { order_by => { artc => 'source_send_time' }, limit => 1 }
222 )->[0]; # Any transit for this barcode?
224 if ($transit) { # yes! unwrap it.
226 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
227 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
229 my $success_event = new OpenILS::Event(
230 "SUCCESS", "payload" => {"reservation" => $reservation}
232 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
233 if (my $copy = $circulator->editor->search_asset_copy([
234 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
235 ])->[0]) { # got a copy
236 $copy->status( $transit->copy_status );
237 $copy->editor($circulator->editor->requestor->id);
238 $copy->edit_date('now');
239 $circulator->editor->update_asset_copy($copy);
240 $success_event->{"payload"}->{"record"} =
241 $U->record_to_mvr($copy->call_number->record);
242 $success_event->{"payload"}->{"volume"} = $copy->call_number;
243 $copy->call_number($copy->call_number->id);
244 $success_event->{"payload"}->{"copy"} = $copy;
248 $transit->dest_recv_time('now');
249 $circulator->editor->update_action_reservation_transit_copy( $transit );
251 $circulator->editor->commit;
252 # Formerly this branch just stopped here. Argh!
253 $conn->respond_complete($success_event);
261 # --------------------------------------------------------------------------
262 # Go ahead and load the script runner to make sure we have all
263 # of the objects we need
264 # --------------------------------------------------------------------------
266 if ($circulator->use_booking) {
267 $circulator->is_res_checkin($circulator->is_checkin(1))
268 if $api =~ /reservation.return/ or (
269 $api =~ /checkin/ and $circulator->seems_like_reservation()
272 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
275 $circulator->is_renewal(1) if $api =~ /renew/;
276 $circulator->is_checkin(1) if $api =~ /checkin/;
278 $circulator->mk_env();
279 $circulator->noop(1) if $circulator->claims_never_checked_out;
281 if($legacy_script_support and not $circulator->is_checkin) {
282 $circulator->mk_script_runner();
283 $circulator->legacy_script_support(1);
284 $circulator->circ_permit_patron($scripts{circ_permit_patron});
285 $circulator->circ_permit_copy($scripts{circ_permit_copy});
286 $circulator->circ_duration($scripts{circ_duration});
287 $circulator->circ_permit_renew($scripts{circ_permit_renew});
289 return circ_events($circulator) if $circulator->bail_out;
292 $circulator->override(1) if $api =~ /override/o;
294 if( $api =~ /checkout\.permit/ ) {
295 $circulator->do_permit();
297 } elsif( $api =~ /checkout.full/ ) {
299 # requesting a precat checkout implies that any required
300 # overrides have been performed. Go ahead and re-override.
301 $circulator->skip_permit_key(1);
302 $circulator->override(1) if $circulator->request_precat;
303 $circulator->do_permit();
304 $circulator->is_checkout(1);
305 unless( $circulator->bail_out ) {
306 $circulator->events([]);
307 $circulator->do_checkout();
310 } elsif( $circulator->is_res_checkout ) {
311 $circulator->do_reservation_pickup();
313 } elsif( $api =~ /inspect/ ) {
314 my $data = $circulator->do_inspect();
315 $circulator->editor->rollback;
318 } elsif( $api =~ /checkout/ ) {
319 $circulator->is_checkout(1);
320 $circulator->do_checkout();
322 } elsif( $circulator->is_res_checkin ) {
323 $circulator->do_reservation_return();
324 $circulator->do_checkin() if ($circulator->copy());
325 } elsif( $api =~ /checkin/ ) {
326 $circulator->do_checkin();
328 } elsif( $api =~ /renew/ ) {
329 $circulator->is_renewal(1);
330 $circulator->do_renew();
333 if( $circulator->bail_out ) {
336 # make sure no success event accidentally slip in
338 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
341 my @e = @{$circulator->events};
342 push( @ee, $_->{textcode} ) for @e;
343 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
345 $circulator->editor->rollback;
349 $circulator->editor->commit;
352 $conn->respond_complete(circ_events($circulator));
354 $circulator->script_runner->cleanup if $circulator->script_runner;
356 return undef if $circulator->bail_out;
358 $circulator->do_hold_notify($circulator->notify_hold)
359 if $circulator->notify_hold;
360 $circulator->retarget_holds if $circulator->retarget;
361 $circulator->append_reading_list;
362 $circulator->make_trigger_events;
369 my @e = @{$circ->events};
370 # if we have multiple events, SUCCESS should not be one of them;
371 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
372 return (@e == 1) ? $e[0] : \@e;
376 sub translate_legacy_args {
379 if( $$args{barcode} ) {
380 $$args{copy_barcode} = $$args{barcode};
381 delete $$args{barcode};
384 if( $$args{copyid} ) {
385 $$args{copy_id} = $$args{copyid};
386 delete $$args{copyid};
389 if( $$args{patronid} ) {
390 $$args{patron_id} = $$args{patronid};
391 delete $$args{patronid};
394 if( $$args{patron} and !ref($$args{patron}) ) {
395 $$args{patron_id} = $$args{patron};
396 delete $$args{patron};
400 if( $$args{noncat} ) {
401 $$args{is_noncat} = $$args{noncat};
402 delete $$args{noncat};
405 if( $$args{precat} ) {
406 $$args{is_precat} = $$args{request_precat} = $$args{precat};
407 delete $$args{precat};
413 # --------------------------------------------------------------------------
414 # This package actually manages all of the circulation logic
415 # --------------------------------------------------------------------------
416 package OpenILS::Application::Circ::Circulator;
417 use strict; use warnings;
418 use vars q/$AUTOLOAD/;
420 use OpenILS::Utils::Fieldmapper;
421 use OpenSRF::Utils::Cache;
422 use Digest::MD5 qw(md5_hex);
423 use DateTime::Format::ISO8601;
424 use OpenILS::Utils::PermitHold;
425 use OpenSRF::Utils qw/:datetime/;
426 use OpenSRF::Utils::SettingsClient;
427 use OpenILS::Application::Circ::Holds;
428 use OpenILS::Application::Circ::Transit;
429 use OpenSRF::Utils::Logger qw(:logger);
430 use OpenILS::Utils::CStoreEditor qw/:funcs/;
431 use OpenILS::Application::Circ::ScriptBuilder;
432 use OpenILS::Const qw/:const/;
433 use OpenILS::Utils::Penalty;
434 use OpenILS::Application::Circ::CircCommon;
437 my $CC = "OpenILS::Application::Circ::CircCommon";
438 my $holdcode = "OpenILS::Application::Circ::Holds";
439 my $transcode = "OpenILS::Application::Circ::Transit";
445 # --------------------------------------------------------------------------
446 # Add a pile of automagic getter/setter methods
447 # --------------------------------------------------------------------------
448 my @AUTOLOAD_FIELDS = qw/
495 recurring_fines_level
508 cancelled_hold_transit
515 circ_matrix_matchpoint
517 legacy_script_support
527 claims_never_checked_out
545 my $type = ref($self) or die "$self is not an object";
547 my $name = $AUTOLOAD;
550 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
551 $logger->error("circulator: $type: invalid autoload field: $name");
552 die "$type: invalid autoload field: $name\n"
557 *{"${type}::${name}"} = sub {
560 $s->{$name} = $v if defined $v;
564 return $self->$name($data);
569 my( $class, $auth, %args ) = @_;
570 $class = ref($class) || $class;
571 my $self = bless( {}, $class );
574 $self->editor(new_editor(xact => 1, authtoken => $auth));
576 unless( $self->editor->checkauth ) {
577 $self->bail_on_events($self->editor->event);
581 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
583 $self->$_($args{$_}) for keys %args;
586 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
588 # if this is a renewal, default to desk_renewal
589 $self->desk_renewal(1) unless
590 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
592 $self->capture('') unless $self->capture;
594 unless(%user_groups) {
595 my $gps = $self->editor->retrieve_all_permission_grp_tree;
596 %user_groups = map { $_->id => $_ } @$gps;
603 # --------------------------------------------------------------------------
604 # True if we should discontinue processing
605 # --------------------------------------------------------------------------
607 my( $self, $bool ) = @_;
608 if( defined $bool ) {
609 $logger->info("circulator: BAILING OUT") if $bool;
610 $self->{bail_out} = $bool;
612 return $self->{bail_out};
617 my( $self, @evts ) = @_;
620 $e->{payload} = $self->copy if
621 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
623 $logger->info("circulator: pushing event ".$e->{textcode});
624 push( @{$self->events}, $e ) unless
625 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
631 return '' if $self->skip_permit_key;
632 my $key = md5_hex( time() . rand() . "$$" );
633 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
634 return $self->permit_key($key);
637 sub check_permit_key {
639 return 1 if $self->skip_permit_key;
640 my $key = $self->permit_key;
641 return 0 unless $key;
642 my $k = "oils_permit_key_$key";
643 my $one = $self->cache_handle->get_cache($k);
644 $self->cache_handle->delete_cache($k);
645 return ($one) ? 1 : 0;
648 sub seems_like_reservation {
651 # Some words about the following method:
652 # 1) It requires the VIEW_USER permission, but that's not an
653 # issue, right, since all staff should have that?
654 # 2) It returns only one reservation at a time, even if an item can be
655 # and is currently overbooked. Hmmm....
656 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
657 my $result = $booking_ses->request(
658 "open-ils.booking.reservations.by_returnable_resource_barcode",
659 $self->editor->authtoken,
662 $booking_ses->disconnect;
664 return $self->bail_on_events($result) if defined $U->event_code($result);
667 $self->reservation(shift @$result);
675 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
676 sub save_trimmed_copy {
677 my ($self, $copy) = @_;
680 $self->volume($copy->call_number);
681 $self->title($self->volume->record);
682 $self->copy->call_number($self->volume->id);
683 $self->volume->record($self->title->id);
684 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
685 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
686 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
687 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
693 my $e = $self->editor;
695 # --------------------------------------------------------------------------
696 # Grab the fleshed copy
697 # --------------------------------------------------------------------------
698 unless($self->is_noncat) {
701 $copy = $e->retrieve_asset_copy(
702 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
704 } elsif( $self->copy_barcode ) {
706 $copy = $e->search_asset_copy(
707 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
708 } elsif( $self->reservation ) {
709 my $res = $e->json_query(
711 "select" => {"acp" => ["id"]},
716 "field" => "barcode",
720 "field" => "current_resource"
728 "id" => (ref $self->reservation) ?
729 $self->reservation->id : $self->reservation
734 if (ref $res eq "ARRAY" and scalar @$res) {
735 $logger->info("circulator: mapped reservation " .
736 $self->reservation . " to copy " . $res->[0]->{"id"});
737 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
742 $self->save_trimmed_copy($copy);
744 # We can't renew if there is no copy
745 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
746 if $self->is_renewal;
751 # --------------------------------------------------------------------------
753 # --------------------------------------------------------------------------
757 flesh_fields => {au => [ qw/ card / ]}
760 if( $self->patron_id ) {
761 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
762 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
764 } elsif( $self->patron_barcode ) {
766 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
767 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
768 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
770 $patron = $e->retrieve_actor_user($card->usr)
771 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
773 # Use the card we looked up, not the patron's primary, for card active checks
774 $patron->card($card);
777 if( my $copy = $self->copy ) {
780 $flesh->{flesh_fields}->{circ} = ['usr'];
782 my $circ = $e->search_action_circulation([
783 {target_copy => $copy->id, checkin_time => undef}, $flesh
787 $patron = $circ->usr;
788 $circ->usr($patron->id); # de-flesh for consistency
794 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
795 unless $self->patron($patron) or $self->is_checkin;
797 unless($self->is_checkin) {
799 # Check for inactivity and patron reg. expiration
801 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
802 unless $U->is_true($patron->active);
804 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
805 unless $U->is_true($patron->card->active);
807 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
808 cleanse_ISO8601($patron->expire_date));
810 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
811 if( CORE::time > $expire->epoch ) ;
815 # --------------------------------------------------------------------------
816 # This builds the script runner environment and fetches most of the
818 # --------------------------------------------------------------------------
819 sub mk_script_runner {
825 qw/copy copy_barcode copy_id patron
826 patron_id patron_barcode volume title editor/;
828 # Translate our objects into the ScriptBuilder args hash
829 $$args{$_} = $self->$_() for @fields;
831 $args->{ignore_user_status} = 1 if $self->is_checkin;
832 $$args{fetch_patron_by_circ_copy} = 1;
833 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
835 if( my $pco = $self->pending_checkouts ) {
836 $logger->info("circulator: we were given a pending checkouts number of $pco");
837 $$args{patronItemsOut} = $pco;
840 # This fetches most of the objects we need
841 $self->script_runner(
842 OpenILS::Application::Circ::ScriptBuilder->build($args));
844 # Now we translate the ScriptBuilder objects back into self
845 $self->$_($$args{$_}) for @fields;
847 my @evts = @{$args->{_events}} if $args->{_events};
849 $logger->debug("circulator: script builder returned events: @evts") if @evts;
853 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
854 if(!$self->is_noncat and
856 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
860 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
861 return $self->bail_on_events(@e);
866 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
867 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
868 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
869 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
873 # We can't renew if there is no copy
874 return $self->bail_on_events(@evts) if
875 $self->is_renewal and !$self->copy;
877 # Set some circ-specific flags in the script environment
878 my $evt = "environment";
879 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
881 if( $self->is_noncat ) {
882 $self->script_runner->insert("$evt.isNonCat", 1);
883 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
886 if( $self->is_precat ) {
887 $self->script_runner->insert("environment.isPrecat", 1, 1);
890 $self->script_runner->add_path( $_ ) for @$script_libs;
895 # --------------------------------------------------------------------------
896 # Does the circ permit work
897 # --------------------------------------------------------------------------
901 $self->log_me("do_permit()");
903 unless( $self->editor->requestor->id == $self->patron->id ) {
904 return $self->bail_on_events($self->editor->event)
905 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
908 $self->check_captured_holds();
909 $self->do_copy_checks();
910 return if $self->bail_out;
911 $self->run_patron_permit_scripts();
912 $self->run_copy_permit_scripts()
913 unless $self->is_precat or $self->is_noncat;
914 $self->check_item_deposit_events();
915 $self->override_events();
916 return if $self->bail_out;
918 if($self->is_precat and not $self->request_precat) {
921 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
922 return $self->bail_out(1) unless $self->is_renewal;
926 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
929 sub check_item_deposit_events {
931 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
932 if $self->is_deposit and not $self->is_deposit_exempt;
933 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
934 if $self->is_rental and not $self->is_rental_exempt;
937 # returns true if the user is not required to pay deposits
938 sub is_deposit_exempt {
940 my $pid = (ref $self->patron->profile) ?
941 $self->patron->profile->id : $self->patron->profile;
942 my $groups = $U->ou_ancestor_setting_value(
943 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
944 for my $grp (@$groups) {
945 return 1 if $self->is_group_descendant($grp, $pid);
950 # returns true if the user is not required to pay rental fees
951 sub is_rental_exempt {
953 my $pid = (ref $self->patron->profile) ?
954 $self->patron->profile->id : $self->patron->profile;
955 my $groups = $U->ou_ancestor_setting_value(
956 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
957 for my $grp (@$groups) {
958 return 1 if $self->is_group_descendant($grp, $pid);
963 sub is_group_descendant {
964 my($self, $p_id, $c_id) = @_;
965 return 0 unless defined $p_id and defined $c_id;
966 return 1 if $c_id == $p_id;
967 while(my $grp = $user_groups{$c_id}) {
968 $c_id = $grp->parent;
969 return 0 unless defined $c_id;
970 return 1 if $c_id == $p_id;
975 sub check_captured_holds {
977 my $copy = $self->copy;
978 my $patron = $self->patron;
980 return undef unless $copy;
982 my $s = $U->copy_status($copy->status)->id;
983 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
984 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
986 # Item is on the holds shelf, make sure it's going to the right person
987 my $hold = $self->editor->search_action_hold_request(
990 current_copy => $copy->id ,
991 capture_time => { '!=' => undef },
992 cancel_time => undef,
993 fulfillment_time => undef
999 if ($hold and $hold->usr == $patron->id) {
1000 $self->checkout_is_for_hold(1);
1004 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1006 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1010 sub do_copy_checks {
1012 my $copy = $self->copy;
1013 return unless $copy;
1015 my $stat = $U->copy_status($copy->status)->id;
1017 # We cannot check out a copy if it is in-transit
1018 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1019 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1022 $self->handle_claims_returned();
1023 return if $self->bail_out;
1025 # no claims returned circ was found, check if there is any open circ
1026 unless( $self->is_renewal ) {
1028 my $circs = $self->editor->search_action_circulation(
1029 { target_copy => $copy->id, checkin_time => undef }
1032 if(my $old_circ = $circs->[0]) { # an open circ was found
1034 my $payload = {copy => $copy};
1036 if($old_circ->usr == $self->patron->id) {
1038 $payload->{old_circ} = $old_circ;
1040 # If there is an open circulation on the checkout item and an auto-renew
1041 # interval is defined, inform the caller that they should go
1042 # ahead and renew the item instead of warning about open circulations.
1044 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1046 'circ.checkout_auto_renew_age',
1050 if($auto_renew_intvl) {
1051 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1052 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1054 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1055 $payload->{auto_renew} = 1;
1060 return $self->bail_on_events(
1061 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1067 my $LEGACY_CIRC_EVENT_MAP = {
1068 'no_item' => 'ITEM_NOT_CATALOGED',
1069 'actor.usr.barred' => 'PATRON_BARRED',
1070 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1071 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1072 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1073 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1074 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1075 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1076 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1077 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1081 # ---------------------------------------------------------------------
1082 # This pushes any patron-related events into the list but does not
1083 # set bail_out for any events
1084 # ---------------------------------------------------------------------
1085 sub run_patron_permit_scripts {
1087 my $runner = $self->script_runner;
1088 my $patronid = $self->patron->id;
1092 if(!$self->legacy_script_support) {
1094 my $results = $self->run_indb_circ_test;
1095 unless($self->circ_test_success) {
1096 my @trimmed_results;
1098 if ($self->is_noncat) {
1099 # no_item result is OK during noncat checkout
1100 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1104 if ($self->checkout_is_for_hold) {
1105 # if this checkout will fulfill a hold, ignore CIRC blocks
1106 # and rely instead on the (later-checked) FULFILL block
1108 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1109 my $fblock_pens = $self->editor->search_config_standing_penalty(
1110 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1112 for my $res (@$results) {
1113 my $name = $res->{fail_part} || '';
1114 next if grep {$_->name eq $name} @$fblock_pens;
1115 push(@trimmed_results, $res);
1119 # not for hold or noncat
1120 @trimmed_results = @$results;
1124 # update the final set of test results
1125 $self->matrix_test_result(\@trimmed_results);
1127 push @allevents, $self->matrix_test_result_events;
1132 # ---------------------------------------------------------------------
1133 # # Now run the patron permit script
1134 # ---------------------------------------------------------------------
1135 $runner->load($self->circ_permit_patron);
1136 my $result = $runner->run or
1137 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1139 my $patron_events = $result->{events};
1141 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1142 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1143 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1144 $penalties = $penalties->{fatal_penalties};
1146 for my $pen (@$penalties) {
1147 # CIRC blocks are ignored if this is a FULFILL scenario
1148 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1149 my $event = OpenILS::Event->new($pen->name);
1150 $event->{desc} = $pen->label;
1151 push(@allevents, $event);
1154 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1158 $_->{payload} = $self->copy if
1159 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1162 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1164 $self->push_events(@allevents);
1167 sub matrix_test_result_codes {
1169 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1172 sub matrix_test_result_events {
1175 my $event = new OpenILS::Event(
1176 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1178 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1180 } (@{$self->matrix_test_result});
1183 sub run_indb_circ_test {
1185 return $self->matrix_test_result if $self->matrix_test_result;
1187 my $dbfunc = ($self->is_renewal) ?
1188 'action.item_user_renew_test' : 'action.item_user_circ_test';
1190 if( $self->is_precat && $self->request_precat) {
1191 $self->make_precat_copy;
1192 return if $self->bail_out;
1195 my $results = $self->editor->json_query(
1199 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1205 $self->circ_test_success($U->is_true($results->[0]->{success}));
1207 if(my $mp = $results->[0]->{matchpoint}) {
1208 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1209 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1210 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1211 if(defined($results->[0]->{renewals})) {
1212 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1214 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1215 if(defined($results->[0]->{grace_period})) {
1216 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1218 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1219 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1220 # Grab the *last* response for limit_groups, where it is more likely to be filled
1221 $self->limit_groups($results->[-1]->{limit_groups});
1224 return $self->matrix_test_result($results);
1227 # ---------------------------------------------------------------------
1228 # given a use and copy, this will calculate the circulation policy
1229 # parameters. Only works with in-db circ.
1230 # ---------------------------------------------------------------------
1234 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1236 $self->run_indb_circ_test;
1239 circ_test_success => $self->circ_test_success,
1240 failure_events => [],
1241 failure_codes => [],
1242 matchpoint => $self->circ_matrix_matchpoint
1245 unless($self->circ_test_success) {
1246 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1247 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1250 if($self->circ_matrix_matchpoint) {
1251 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1252 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1253 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1254 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1256 my $policy = $self->get_circ_policy(
1257 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1259 $$results{$_} = $$policy{$_} for keys %$policy;
1265 # ---------------------------------------------------------------------
1266 # Loads the circ policy info for duration, recurring fine, and max
1267 # fine based on the current copy
1268 # ---------------------------------------------------------------------
1269 sub get_circ_policy {
1270 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1273 duration_rule => $duration_rule->name,
1274 recurring_fine_rule => $recurring_fine_rule->name,
1275 max_fine_rule => $max_fine_rule->name,
1276 max_fine => $self->get_max_fine_amount($max_fine_rule),
1277 fine_interval => $recurring_fine_rule->recurrence_interval,
1278 renewal_remaining => $duration_rule->max_renewals,
1279 grace_period => $recurring_fine_rule->grace_period
1282 if($hard_due_date) {
1283 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1284 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1287 $policy->{duration_date_ceiling} = undef;
1288 $policy->{duration_date_ceiling_force} = undef;
1291 $policy->{duration} = $duration_rule->shrt
1292 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1293 $policy->{duration} = $duration_rule->normal
1294 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1295 $policy->{duration} = $duration_rule->extended
1296 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1298 $policy->{recurring_fine} = $recurring_fine_rule->low
1299 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1300 $policy->{recurring_fine} = $recurring_fine_rule->normal
1301 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1302 $policy->{recurring_fine} = $recurring_fine_rule->high
1303 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1308 sub get_max_fine_amount {
1310 my $max_fine_rule = shift;
1311 my $max_amount = $max_fine_rule->amount;
1313 # if is_percent is true then the max->amount is
1314 # use as a percentage of the copy price
1315 if ($U->is_true($max_fine_rule->is_percent)) {
1316 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1317 $max_amount = $price * $max_fine_rule->amount / 100;
1319 $U->ou_ancestor_setting_value(
1321 'circ.max_fine.cap_at_price',
1325 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1326 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1334 sub run_copy_permit_scripts {
1336 my $copy = $self->copy || return;
1337 my $runner = $self->script_runner;
1341 if(!$self->legacy_script_support) {
1342 my $results = $self->run_indb_circ_test;
1343 push @allevents, $self->matrix_test_result_events
1344 unless $self->circ_test_success;
1347 # ---------------------------------------------------------------------
1348 # Capture all of the copy permit events
1349 # ---------------------------------------------------------------------
1350 $runner->load($self->circ_permit_copy);
1351 my $result = $runner->run or
1352 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1353 my $copy_events = $result->{events};
1355 # ---------------------------------------------------------------------
1356 # Now collect all of the events together
1357 # ---------------------------------------------------------------------
1358 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1361 # See if this copy has an alert message
1362 my $ae = $self->check_copy_alert();
1363 push( @allevents, $ae ) if $ae;
1365 # uniquify the events
1366 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1367 @allevents = values %hash;
1369 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1371 $self->push_events(@allevents);
1375 sub check_copy_alert {
1377 return undef if $self->is_renewal;
1378 return OpenILS::Event->new(
1379 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1380 if $self->copy and $self->copy->alert_message;
1386 # --------------------------------------------------------------------------
1387 # If the call is overriding and has permissions to override every collected
1388 # event, the are cleared. Any event that the caller does not have
1389 # permission to override, will be left in the event list and bail_out will
1391 # XXX We need code in here to cancel any holds/transits on copies
1392 # that are being force-checked out
1393 # --------------------------------------------------------------------------
1394 sub override_events {
1396 my @events = @{$self->events};
1397 return unless @events;
1398 my $oargs = $self->override_args;
1400 if(!$self->override) {
1401 return $self->bail_out(1)
1402 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1407 for my $e (@events) {
1408 my $tc = $e->{textcode};
1409 next if $tc eq 'SUCCESS';
1410 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1411 my $ov = "$tc.override";
1412 $logger->info("circulator: attempting to override event: $ov");
1414 return $self->bail_on_events($self->editor->event)
1415 unless( $self->editor->allowed($ov) );
1417 return $self->bail_out(1);
1423 # --------------------------------------------------------------------------
1424 # If there is an open claimsreturn circ on the requested copy, close the
1425 # circ if overriding, otherwise bail out
1426 # --------------------------------------------------------------------------
1427 sub handle_claims_returned {
1429 my $copy = $self->copy;
1431 my $CR = $self->editor->search_action_circulation(
1433 target_copy => $copy->id,
1434 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1435 checkin_time => undef,
1439 return unless ($CR = $CR->[0]);
1443 # - If the caller has set the override flag, we will check the item in
1444 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1446 $CR->checkin_time('now');
1447 $CR->checkin_scan_time('now');
1448 $CR->checkin_lib($self->circ_lib);
1449 $CR->checkin_workstation($self->editor->requestor->wsid);
1450 $CR->checkin_staff($self->editor->requestor->id);
1452 $evt = $self->editor->event
1453 unless $self->editor->update_action_circulation($CR);
1456 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1459 $self->bail_on_events($evt) if $evt;
1464 # --------------------------------------------------------------------------
1465 # This performs the checkout
1466 # --------------------------------------------------------------------------
1470 $self->log_me("do_checkout()");
1472 # make sure perms are good if this isn't a renewal
1473 unless( $self->is_renewal ) {
1474 return $self->bail_on_events($self->editor->event)
1475 unless( $self->editor->allowed('COPY_CHECKOUT') );
1478 # verify the permit key
1479 unless( $self->check_permit_key ) {
1480 if( $self->permit_override ) {
1481 return $self->bail_on_events($self->editor->event)
1482 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1484 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1488 # if this is a non-cataloged circ, build the circ and finish
1489 if( $self->is_noncat ) {
1490 $self->checkout_noncat;
1492 OpenILS::Event->new('SUCCESS',
1493 payload => { noncat_circ => $self->circ }));
1497 if( $self->is_precat ) {
1498 $self->make_precat_copy;
1499 return if $self->bail_out;
1501 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1502 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1505 $self->do_copy_checks;
1506 return if $self->bail_out;
1508 $self->run_checkout_scripts();
1509 return if $self->bail_out;
1511 $self->build_checkout_circ_object();
1512 return if $self->bail_out;
1514 my $modify_to_start = $self->booking_adjusted_due_date();
1515 return if $self->bail_out;
1517 $self->apply_modified_due_date($modify_to_start);
1518 return if $self->bail_out;
1520 return $self->bail_on_events($self->editor->event)
1521 unless $self->editor->create_action_circulation($self->circ);
1523 # refresh the circ to force local time zone for now
1524 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1526 if($self->limit_groups) {
1527 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1530 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1532 return if $self->bail_out;
1534 $self->apply_deposit_fee();
1535 return if $self->bail_out;
1537 $self->handle_checkout_holds();
1538 return if $self->bail_out;
1540 # ------------------------------------------------------------------------------
1541 # Update the patron penalty info in the DB. Run it for permit-overrides
1542 # since the penalties are not updated during the permit phase
1543 # ------------------------------------------------------------------------------
1544 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1546 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1549 if($self->is_renewal) {
1550 # flesh the billing summary for the checked-in circ
1551 $pcirc = $self->editor->retrieve_action_circulation([
1553 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1558 OpenILS::Event->new('SUCCESS',
1560 copy => $U->unflesh_copy($self->copy),
1561 volume => $self->volume,
1562 circ => $self->circ,
1564 holds_fulfilled => $self->fulfilled_holds,
1565 deposit_billing => $self->deposit_billing,
1566 rental_billing => $self->rental_billing,
1567 parent_circ => $pcirc,
1568 patron => ($self->return_patron) ? $self->patron : undef,
1569 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1575 sub apply_deposit_fee {
1577 my $copy = $self->copy;
1579 ($self->is_deposit and not $self->is_deposit_exempt) or
1580 ($self->is_rental and not $self->is_rental_exempt);
1582 return if $self->is_deposit and $self->skip_deposit_fee;
1583 return if $self->is_rental and $self->skip_rental_fee;
1585 my $bill = Fieldmapper::money::billing->new;
1586 my $amount = $copy->deposit_amount;
1590 if($self->is_deposit) {
1591 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1593 $self->deposit_billing($bill);
1595 $billing_type = OILS_BILLING_TYPE_RENTAL;
1597 $self->rental_billing($bill);
1600 $bill->xact($self->circ->id);
1601 $bill->amount($amount);
1602 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1603 $bill->billing_type($billing_type);
1604 $bill->btype($btype);
1605 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1607 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1612 my $copy = $self->copy;
1614 my $stat = $copy->status if ref $copy->status;
1615 my $loc = $copy->location if ref $copy->location;
1616 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1618 $copy->status($stat->id) if $stat;
1619 $copy->location($loc->id) if $loc;
1620 $copy->circ_lib($circ_lib->id) if $circ_lib;
1621 $copy->editor($self->editor->requestor->id);
1622 $copy->edit_date('now');
1623 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1625 return $self->bail_on_events($self->editor->event)
1626 unless $self->editor->update_asset_copy($self->copy);
1628 $copy->status($U->copy_status($copy->status));
1629 $copy->location($loc) if $loc;
1630 $copy->circ_lib($circ_lib) if $circ_lib;
1633 sub update_reservation {
1635 my $reservation = $self->reservation;
1637 my $usr = $reservation->usr;
1638 my $target_rt = $reservation->target_resource_type;
1639 my $target_r = $reservation->target_resource;
1640 my $current_r = $reservation->current_resource;
1642 $reservation->usr($usr->id) if ref $usr;
1643 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1644 $reservation->target_resource($target_r->id) if ref $target_r;
1645 $reservation->current_resource($current_r->id) if ref $current_r;
1647 return $self->bail_on_events($self->editor->event)
1648 unless $self->editor->update_booking_reservation($self->reservation);
1651 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1652 $self->reservation($reservation);
1656 sub bail_on_events {
1657 my( $self, @evts ) = @_;
1658 $self->push_events(@evts);
1662 # ------------------------------------------------------------------------------
1663 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1664 # affects copies that will fulfill holds and CIRC affects all other copies.
1665 # If blocks exists, bail, push Events onto the event pile, and return true.
1666 # ------------------------------------------------------------------------------
1667 sub check_hold_fulfill_blocks {
1670 # See if the user has any penalties applied that prevent hold fulfillment
1671 my $pens = $self->editor->json_query({
1672 select => {csp => ['name', 'label']},
1673 from => {ausp => {csp => {}}},
1676 usr => $self->patron->id,
1677 org_unit => $U->get_org_full_path($self->circ_lib),
1679 {stop_date => undef},
1680 {stop_date => {'>' => 'now'}}
1683 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1687 return 0 unless @$pens;
1689 for my $pen (@$pens) {
1690 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1691 my $event = OpenILS::Event->new($pen->{name});
1692 $event->{desc} = $pen->{label};
1693 $self->push_events($event);
1696 $self->override_events;
1697 return $self->bail_out;
1701 # ------------------------------------------------------------------------------
1702 # When an item is checked out, see if we can fulfill a hold for this patron
1703 # ------------------------------------------------------------------------------
1704 sub handle_checkout_holds {
1706 my $copy = $self->copy;
1707 my $patron = $self->patron;
1709 my $e = $self->editor;
1710 $self->fulfilled_holds([]);
1712 # non-cats can't fulfill a hold
1713 return if $self->is_noncat;
1715 my $hold = $e->search_action_hold_request({
1716 current_copy => $copy->id ,
1717 cancel_time => undef,
1718 fulfillment_time => undef,
1720 {expire_time => undef},
1721 {expire_time => {'>' => 'now'}}
1725 if($hold and $hold->usr != $patron->id) {
1726 # reset the hold since the copy is now checked out
1728 $logger->info("circulator: un-targeting hold ".$hold->id.
1729 " because copy ".$copy->id." is getting checked out");
1731 $hold->clear_prev_check_time;
1732 $hold->clear_current_copy;
1733 $hold->clear_capture_time;
1734 $hold->clear_shelf_time;
1735 $hold->clear_shelf_expire_time;
1736 $hold->clear_current_shelf_lib;
1738 return $self->bail_on_event($e->event)
1739 unless $e->update_action_hold_request($hold);
1745 $hold = $self->find_related_user_hold($copy, $patron) or return;
1746 $logger->info("circulator: found related hold to fulfill in checkout");
1749 return if $self->check_hold_fulfill_blocks;
1751 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1753 # if the hold was never officially captured, capture it.
1754 $hold->current_copy($copy->id);
1755 $hold->capture_time('now') unless $hold->capture_time;
1756 $hold->fulfillment_time('now');
1757 $hold->fulfillment_staff($e->requestor->id);
1758 $hold->fulfillment_lib($self->circ_lib);
1760 return $self->bail_on_events($e->event)
1761 unless $e->update_action_hold_request($hold);
1763 return $self->fulfilled_holds([$hold->id]);
1767 # ------------------------------------------------------------------------------
1768 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1769 # the patron directly targets the checked out item, see if there is another hold
1770 # for the patron that could be fulfilled by the checked out item. Fulfill the
1771 # oldest hold and only fulfill 1 of them.
1773 # For "another hold":
1775 # First, check for one that the copy matches via hold_copy_map, ensuring that
1776 # *any* hold type that this copy could fill may end up filled.
1778 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1779 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1780 # that are non-requestable to count as capturing those hold types.
1781 # ------------------------------------------------------------------------------
1782 sub find_related_user_hold {
1783 my($self, $copy, $patron) = @_;
1784 my $e = $self->editor;
1786 # holds on precat copies are always copy-level, so this call will
1787 # always return undef. Exit early.
1788 return undef if $self->is_precat;
1790 return undef unless $U->ou_ancestor_setting_value(
1791 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1793 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1795 select => {ahr => ['id']},
1804 fkey => 'current_copy',
1805 type => 'left' # there may be no current_copy
1812 fulfillment_time => undef,
1813 cancel_time => undef,
1815 {expire_time => undef},
1816 {expire_time => {'>' => 'now'}}
1820 target_copy => $self->copy->id
1824 {id => undef}, # left-join copy may be nonexistent
1825 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1829 order_by => {ahr => {request_time => {direction => 'asc'}}},
1833 my $hold_info = $e->json_query($args)->[0];
1834 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1835 return undef if $U->ou_ancestor_setting_value(
1836 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1838 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1840 select => {ahr => ['id']},
1845 fkey => 'current_copy',
1846 type => 'left' # there may be no current_copy
1853 fulfillment_time => undef,
1854 cancel_time => undef,
1856 {expire_time => undef},
1857 {expire_time => {'>' => 'now'}}
1864 target => $self->volume->id
1870 target => $self->title->id
1876 {id => undef}, # left-join copy may be nonexistent
1877 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1881 order_by => {ahr => {request_time => {direction => 'asc'}}},
1885 $hold_info = $e->json_query($args)->[0];
1886 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1891 sub run_checkout_scripts {
1896 my $runner = $self->script_runner;
1905 my $hard_due_date_name;
1907 if(!$self->legacy_script_support) {
1908 $self->run_indb_circ_test();
1909 $duration = $self->circ_matrix_matchpoint->duration_rule;
1910 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1911 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1912 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1916 $runner->load($self->circ_duration);
1918 my $result = $runner->run or
1919 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1921 $duration_name = $result->{durationRule};
1922 $recurring_name = $result->{recurringFinesRule};
1923 $max_fine_name = $result->{maxFine};
1924 $hard_due_date_name = $result->{hardDueDate};
1927 $duration_name = $duration->name if $duration;
1928 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1931 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1932 return $self->bail_on_events($evt) if ($evt && !$nobail);
1934 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1935 return $self->bail_on_events($evt) if ($evt && !$nobail);
1937 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1938 return $self->bail_on_events($evt) if ($evt && !$nobail);
1940 if($hard_due_date_name) {
1941 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1942 return $self->bail_on_events($evt) if ($evt && !$nobail);
1948 # The item circulates with an unlimited duration
1952 $hard_due_date = undef;
1955 $self->duration_rule($duration);
1956 $self->recurring_fines_rule($recurring);
1957 $self->max_fine_rule($max_fine);
1958 $self->hard_due_date($hard_due_date);
1962 sub build_checkout_circ_object {
1965 my $circ = Fieldmapper::action::circulation->new;
1966 my $duration = $self->duration_rule;
1967 my $max = $self->max_fine_rule;
1968 my $recurring = $self->recurring_fines_rule;
1969 my $hard_due_date = $self->hard_due_date;
1970 my $copy = $self->copy;
1971 my $patron = $self->patron;
1972 my $duration_date_ceiling;
1973 my $duration_date_ceiling_force;
1977 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1978 $duration_date_ceiling = $policy->{duration_date_ceiling};
1979 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1981 my $dname = $duration->name;
1982 my $mname = $max->name;
1983 my $rname = $recurring->name;
1985 if($hard_due_date) {
1986 $hdname = $hard_due_date->name;
1989 $logger->debug("circulator: building circulation ".
1990 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1992 $circ->duration($policy->{duration});
1993 $circ->recurring_fine($policy->{recurring_fine});
1994 $circ->duration_rule($duration->name);
1995 $circ->recurring_fine_rule($recurring->name);
1996 $circ->max_fine_rule($max->name);
1997 $circ->max_fine($policy->{max_fine});
1998 $circ->fine_interval($recurring->recurrence_interval);
1999 $circ->renewal_remaining($duration->max_renewals);
2000 $circ->grace_period($policy->{grace_period});
2004 $logger->info("circulator: copy found with an unlimited circ duration");
2005 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2006 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2007 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2008 $circ->renewal_remaining(0);
2009 $circ->grace_period(0);
2012 $circ->target_copy( $copy->id );
2013 $circ->usr( $patron->id );
2014 $circ->circ_lib( $self->circ_lib );
2015 $circ->workstation($self->editor->requestor->wsid)
2016 if defined $self->editor->requestor->wsid;
2018 # renewals maintain a link to the parent circulation
2019 $circ->parent_circ($self->parent_circ);
2021 if( $self->is_renewal ) {
2022 $circ->opac_renewal('t') if $self->opac_renewal;
2023 $circ->phone_renewal('t') if $self->phone_renewal;
2024 $circ->desk_renewal('t') if $self->desk_renewal;
2025 $circ->renewal_remaining($self->renewal_remaining);
2026 $circ->circ_staff($self->editor->requestor->id);
2030 # if the user provided an overiding checkout time,
2031 # (e.g. the checkout really happened several hours ago), then
2032 # we apply that here. Does this need a perm??
2033 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2034 if $self->checkout_time;
2036 # if a patron is renewing, 'requestor' will be the patron
2037 $circ->circ_staff($self->editor->requestor->id);
2038 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2043 sub do_reservation_pickup {
2046 $self->log_me("do_reservation_pickup()");
2048 $self->reservation->pickup_time('now');
2051 $self->reservation->current_resource &&
2052 $U->is_true($self->reservation->target_resource_type->catalog_item)
2054 # We used to try to set $self->copy and $self->patron here,
2055 # but that should already be done.
2057 $self->run_checkout_scripts(1);
2059 my $duration = $self->duration_rule;
2060 my $max = $self->max_fine_rule;
2061 my $recurring = $self->recurring_fines_rule;
2063 if ($duration && $max && $recurring) {
2064 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2066 my $dname = $duration->name;
2067 my $mname = $max->name;
2068 my $rname = $recurring->name;
2070 $logger->debug("circulator: updating reservation ".
2071 "with duration=$dname, maxfine=$mname, recurring=$rname");
2073 $self->reservation->fine_amount($policy->{recurring_fine});
2074 $self->reservation->max_fine($policy->{max_fine});
2075 $self->reservation->fine_interval($recurring->recurrence_interval);
2078 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2079 $self->update_copy();
2082 $self->reservation->fine_amount(
2083 $self->reservation->target_resource_type->fine_amount
2085 $self->reservation->max_fine(
2086 $self->reservation->target_resource_type->max_fine
2088 $self->reservation->fine_interval(
2089 $self->reservation->target_resource_type->fine_interval
2093 $self->update_reservation();
2096 sub do_reservation_return {
2098 my $request = shift;
2100 $self->log_me("do_reservation_return()");
2102 if (not ref $self->reservation) {
2103 my ($reservation, $evt) =
2104 $U->fetch_booking_reservation($self->reservation);
2105 return $self->bail_on_events($evt) if $evt;
2106 $self->reservation($reservation);
2109 $self->handle_fines(1);
2110 $self->reservation->return_time('now');
2111 $self->update_reservation();
2112 $self->reshelve_copy if $self->copy;
2114 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2115 $self->copy( $self->reservation->current_resource->catalog_item );
2119 sub booking_adjusted_due_date {
2121 my $circ = $self->circ;
2122 my $copy = $self->copy;
2124 return undef unless $self->use_booking;
2128 if( $self->due_date ) {
2130 return $self->bail_on_events($self->editor->event)
2131 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2133 $circ->due_date(cleanse_ISO8601($self->due_date));
2137 return unless $copy and $circ->due_date;
2140 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2141 if (@$booking_items) {
2142 my $booking_item = $booking_items->[0];
2143 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2145 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2146 my $shorten_circ_setting = $resource_type->elbow_room ||
2147 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2150 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2151 my $bookings = $booking_ses->request(
2152 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2153 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2155 $booking_ses->disconnect;
2157 my $dt_parser = DateTime::Format::ISO8601->new;
2158 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2160 for my $bid (@$bookings) {
2162 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2164 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2165 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2167 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2168 if ($booking_start < DateTime->now);
2171 if ($U->is_true($stop_circ_setting)) {
2172 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2174 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2175 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2178 # We set the circ duration here only to affect the logic that will
2179 # later (in a DB trigger) mangle the time part of the due date to
2180 # 11:59pm. Having any circ duration that is not a whole number of
2181 # days is enough to prevent the "correction."
2182 my $new_circ_duration = $due_date->epoch - time;
2183 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2184 $circ->duration("$new_circ_duration seconds");
2186 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2190 return $self->bail_on_events($self->editor->event)
2191 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2197 sub apply_modified_due_date {
2199 my $shift_earlier = shift;
2200 my $circ = $self->circ;
2201 my $copy = $self->copy;
2203 if( $self->due_date ) {
2205 return $self->bail_on_events($self->editor->event)
2206 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2208 $circ->due_date(cleanse_ISO8601($self->due_date));
2212 # if the due_date lands on a day when the location is closed
2213 return unless $copy and $circ->due_date;
2215 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2217 # due-date overlap should be determined by the location the item
2218 # is checked out from, not the owning or circ lib of the item
2219 my $org = $self->circ_lib;
2221 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2222 " with an item due date of ".$circ->due_date );
2224 my $dateinfo = $U->storagereq(
2225 'open-ils.storage.actor.org_unit.closed_date.overlap',
2226 $org, $circ->due_date );
2229 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2230 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2232 # XXX make the behavior more dynamic
2233 # for now, we just push the due date to after the close date
2234 if ($shift_earlier) {
2235 $circ->due_date($dateinfo->{start});
2237 $circ->due_date($dateinfo->{end});
2245 sub create_due_date {
2246 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2248 # if there is a raw time component (e.g. from postgres),
2249 # turn it into an interval that interval_to_seconds can parse
2250 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2252 # for now, use the server timezone. TODO: use workstation org timezone
2253 my $due_date = DateTime->now(time_zone => 'local');
2254 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2256 # add the circ duration
2257 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2260 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2261 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2262 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2267 # return ISO8601 time with timezone
2268 return $due_date->strftime('%FT%T%z');
2273 sub make_precat_copy {
2275 my $copy = $self->copy;
2278 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2280 $copy->editor($self->editor->requestor->id);
2281 $copy->edit_date('now');
2282 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2283 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2284 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2285 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2286 $self->update_copy();
2290 $logger->info("circulator: Creating a new precataloged ".
2291 "copy in checkout with barcode " . $self->copy_barcode);
2293 $copy = Fieldmapper::asset::copy->new;
2294 $copy->circ_lib($self->circ_lib);
2295 $copy->creator($self->editor->requestor->id);
2296 $copy->editor($self->editor->requestor->id);
2297 $copy->barcode($self->copy_barcode);
2298 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2299 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2300 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2302 $copy->dummy_title($self->dummy_title || "");
2303 $copy->dummy_author($self->dummy_author || "");
2304 $copy->dummy_isbn($self->dummy_isbn || "");
2305 $copy->circ_modifier($self->circ_modifier);
2308 # See if we need to override the circ_lib for the copy with a configured circ_lib
2309 # Setting is shortname of the org unit
2310 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2311 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2313 if($precat_circ_lib) {
2314 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2317 $self->bail_on_events($self->editor->event);
2321 $copy->circ_lib($org->id);
2325 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2327 $self->push_events($self->editor->event);
2331 # this is a little bit of a hack, but we need to
2332 # get the copy into the script runner
2333 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2337 sub checkout_noncat {
2343 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2344 my $count = $self->noncat_count || 1;
2345 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2347 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2351 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2352 $self->editor->requestor->id,
2360 $self->push_events($evt);
2368 # If a copy goes into transit and is then checked in before the transit checkin
2369 # interval has expired, push an event onto the overridable events list.
2370 sub check_transit_checkin_interval {
2373 # only concerned with in-transit items
2374 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2376 # no interval, no problem
2377 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2378 return unless $interval;
2380 # capture the transit so we don't have to fetch it again later during checkin
2382 $self->editor->search_action_transit_copy(
2383 {target_copy => $self->copy->id, dest_recv_time => undef}
2387 # transit from X to X for whatever reason has no min interval
2388 return if $self->transit->source == $self->transit->dest;
2390 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2391 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2392 my $horizon = $t_start->add(seconds => $seconds);
2394 # See if we are still within the transit checkin forbidden range
2395 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2396 if $horizon > DateTime->now;
2399 # Retarget local holds at checkin
2400 sub checkin_retarget {
2402 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2403 return unless $self->is_checkin; # Renewals need not be checked
2404 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2405 return if $self->is_precat; # No holds for precats
2406 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2407 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2408 my $status = $U->copy_status($self->copy->status);
2409 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2410 # Specifically target items that are likely new (by status ID)
2411 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2412 my $location = $self->copy->location;
2413 if(!ref($location)) {
2414 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2415 $self->copy->location($location);
2417 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2419 # Fetch holds for the bib
2420 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2421 $self->editor->authtoken,
2424 capture_time => undef, # No touching captured holds
2425 frozen => 'f', # Don't bother with frozen holds
2426 pickup_lib => $self->circ_lib # Only holds actually here
2429 # Error? Skip the step.
2430 return if exists $result->{"ilsevent"};
2434 foreach my $holdlist (keys %{$result}) {
2435 push @$holds, @{$result->{$holdlist}};
2438 return if scalar(@$holds) == 0; # No holds, no retargeting
2440 # Check for parts on this copy
2441 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2442 my %parts_hash = ();
2443 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2445 # Loop over holds in request-ish order
2446 # Stage 1: Get them into request-ish order
2447 # Also grab type and target for skipping low hanging ones
2448 $result = $self->editor->json_query({
2449 "select" => { "ahr" => ["id", "hold_type", "target"] },
2450 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2451 "where" => { "id" => $holds },
2453 { "class" => "pgt", "field" => "hold_priority"},
2454 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2455 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2456 { "class" => "ahr", "field" => "request_time"}
2461 if (ref $result eq "ARRAY" and scalar @$result) {
2462 foreach (@{$result}) {
2463 # Copy level, but not this copy?
2464 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2465 and $_->{target} != $self->copy->id);
2466 # Volume level, but not this volume?
2467 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2468 if(@$parts) { # We have parts?
2470 next if ($_->{hold_type} eq 'T');
2471 # Skip part holds for parts not on this copy
2472 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2474 # No parts, no part holds
2475 next if ($_->{hold_type} eq 'P');
2477 # So much for easy stuff, attempt a retarget!
2478 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2479 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2480 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2488 $self->log_me("do_checkin()");
2490 return $self->bail_on_events(
2491 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2494 $self->check_transit_checkin_interval;
2495 $self->checkin_retarget;
2497 # the renew code and mk_env should have already found our circulation object
2498 unless( $self->circ ) {
2500 my $circs = $self->editor->search_action_circulation(
2501 { target_copy => $self->copy->id, checkin_time => undef });
2503 $self->circ($$circs[0]);
2505 # for now, just warn if there are multiple open circs on a copy
2506 $logger->warn("circulator: we have ".scalar(@$circs).
2507 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2510 my $ignore_stop_fines = undef;
2513 # if this circ is LOST and we are configured to generate overdue
2514 # fines for lost items on checkin (to fill the gap between mark
2515 # lost time and when the fines would have naturally stopped), tell
2516 # the fine generator to ignore the stop-fines value on this circ.
2517 my $stat = $U->copy_status($self->copy->status)->id;
2518 if ($stat == OILS_COPY_STATUS_LOST) {
2519 $ignore_stop_fines = $self->circ->stop_fines if
2520 $U->ou_ancestor_setting_value(
2522 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2527 # run the fine generator against this circ
2528 $self->handle_fines(undef, $ignore_stop_fines);
2531 if( $self->checkin_check_holds_shelf() ) {
2532 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2533 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2534 if($self->fake_hold_dest) {
2535 $self->hold->pickup_lib($self->circ_lib);
2537 $self->checkin_flesh_events;
2541 unless( $self->is_renewal ) {
2542 return $self->bail_on_events($self->editor->event)
2543 unless $self->editor->allowed('COPY_CHECKIN');
2546 $self->push_events($self->check_copy_alert());
2547 $self->push_events($self->check_checkin_copy_status());
2549 # if the circ is marked as 'claims returned', add the event to the list
2550 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2551 if ($self->circ and $self->circ->stop_fines
2552 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2554 $self->check_circ_deposit();
2556 # handle the overridable events
2557 $self->override_events unless $self->is_renewal;
2558 return if $self->bail_out;
2560 if( $self->copy and !$self->transit ) {
2562 $self->editor->search_action_transit_copy(
2563 { target_copy => $self->copy->id, dest_recv_time => undef }
2569 $self->checkin_handle_circ;
2570 return if $self->bail_out;
2571 $self->checkin_changed(1);
2573 } elsif( $self->transit ) {
2574 my $hold_transit = $self->process_received_transit;
2575 $self->checkin_changed(1);
2577 if( $self->bail_out ) {
2578 $self->checkin_flesh_events;
2582 if( my $e = $self->check_checkin_copy_status() ) {
2583 # If the original copy status is special, alert the caller
2584 my $ev = $self->events;
2585 $self->events([$e]);
2586 $self->override_events;
2587 return if $self->bail_out;
2591 if( $hold_transit or
2592 $U->copy_status($self->copy->status)->id
2593 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2596 if( $hold_transit ) {
2597 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2599 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2604 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2606 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2607 $self->reshelve_copy(1);
2608 $self->cancelled_hold_transit(1);
2609 $self->notify_hold(0); # don't notify for cancelled holds
2610 $self->fake_hold_dest(0);
2611 return if $self->bail_out;
2613 } elsif ($hold and $hold->hold_type eq 'R') {
2615 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2616 $self->notify_hold(0); # No need to notify
2617 $self->fake_hold_dest(0);
2618 $self->noop(1); # Don't try and capture for other holds/transits now
2619 $self->update_copy();
2620 $hold->fulfillment_time('now');
2621 $self->bail_on_events($self->editor->event)
2622 unless $self->editor->update_action_hold_request($hold);
2626 # hold transited to correct location
2627 if($self->fake_hold_dest) {
2628 $hold->pickup_lib($self->circ_lib);
2630 $self->checkin_flesh_events;
2635 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2637 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2638 " that is in-transit, but there is no transit.. repairing");
2639 $self->reshelve_copy(1);
2640 return if $self->bail_out;
2643 if( $self->is_renewal ) {
2644 $self->finish_fines_and_voiding;
2645 return if $self->bail_out;
2646 $self->push_events(OpenILS::Event->new('SUCCESS'));
2650 # ------------------------------------------------------------------------------
2651 # Circulations and transits are now closed where necessary. Now go on to see if
2652 # this copy can fulfill a hold or needs to be routed to a different location
2653 # ------------------------------------------------------------------------------
2655 my $needed_for_something = 0; # formerly "needed_for_hold"
2657 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2659 if (!$self->remote_hold) {
2660 if ($self->use_booking) {
2661 my $potential_hold = $self->hold_capture_is_possible;
2662 my $potential_reservation = $self->reservation_capture_is_possible;
2664 if ($potential_hold and $potential_reservation) {
2665 $logger->info("circulator: item could fulfill either hold or reservation");
2666 $self->push_events(new OpenILS::Event(
2667 "HOLD_RESERVATION_CONFLICT",
2668 "hold" => $potential_hold,
2669 "reservation" => $potential_reservation
2671 return if $self->bail_out;
2672 } elsif ($potential_hold) {
2673 $needed_for_something =
2674 $self->attempt_checkin_hold_capture;
2675 } elsif ($potential_reservation) {
2676 $needed_for_something =
2677 $self->attempt_checkin_reservation_capture;
2680 $needed_for_something = $self->attempt_checkin_hold_capture;
2683 return if $self->bail_out;
2685 unless($needed_for_something) {
2686 my $circ_lib = (ref $self->copy->circ_lib) ?
2687 $self->copy->circ_lib->id : $self->copy->circ_lib;
2689 if( $self->remote_hold ) {
2690 $circ_lib = $self->remote_hold->pickup_lib;
2691 $logger->warn("circulator: Copy ".$self->copy->barcode.
2692 " is on a remote hold's shelf, sending to $circ_lib");
2695 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2697 my $suppress_transit = 0;
2699 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2700 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2701 if($suppress_transit_source && $suppress_transit_source->{value}) {
2702 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2703 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2704 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2705 $suppress_transit = 1;
2710 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2711 # copy is where it needs to be, either for hold or reshelving
2713 $self->checkin_handle_precat();
2714 return if $self->bail_out;
2717 # copy needs to transit "home", or stick here if it's a floating copy
2719 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2720 my $res = $self->editor->json_query(
2722 'evergreen.can_float',
2723 $self->copy->floating->id,
2724 $self->copy->circ_lib,
2729 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2731 if ($can_float) { # Yep, floating, stick here
2732 $self->checkin_changed(1);
2733 $self->copy->circ_lib( $self->circ_lib );
2736 my $bc = $self->copy->barcode;
2737 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2738 $self->checkin_build_copy_transit($circ_lib);
2739 return if $self->bail_out;
2740 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2744 } else { # no-op checkin
2745 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2746 $self->checkin_changed(1);
2747 $self->copy->circ_lib( $self->circ_lib );
2752 if($self->claims_never_checked_out and
2753 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2755 # the item was not supposed to be checked out to the user and should now be marked as missing
2756 $self->copy->status(OILS_COPY_STATUS_MISSING);
2760 $self->reshelve_copy unless $needed_for_something;
2763 return if $self->bail_out;
2765 unless($self->checkin_changed) {
2767 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2768 my $stat = $U->copy_status($self->copy->status)->id;
2770 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2771 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2772 $self->bail_out(1); # no need to commit anything
2776 $self->push_events(OpenILS::Event->new('SUCCESS'))
2777 unless @{$self->events};
2780 $self->finish_fines_and_voiding;
2782 OpenILS::Utils::Penalty->calculate_penalties(
2783 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2785 $self->checkin_flesh_events;
2789 sub finish_fines_and_voiding {
2791 return unless $self->circ;
2793 return unless $self->backdate or $self->void_overdues;
2795 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2796 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2798 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2799 $self->editor, $self->circ, $self->backdate, $note);
2801 return $self->bail_on_events($evt) if $evt;
2803 # Make sure the circ is open or closed as necessary.
2804 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2805 return $self->bail_on_events($evt) if $evt;
2811 # if a deposit was payed for this item, push the event
2812 sub check_circ_deposit {
2814 return unless $self->circ;
2815 my $deposit = $self->editor->search_money_billing(
2817 xact => $self->circ->id,
2819 }, {idlist => 1})->[0];
2821 $self->push_events(OpenILS::Event->new(
2822 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2827 my $force = $self->force || shift;
2828 my $copy = $self->copy;
2830 my $stat = $U->copy_status($copy->status)->id;
2833 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2834 $stat != OILS_COPY_STATUS_CATALOGING and
2835 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2836 $stat != OILS_COPY_STATUS_RESHELVING )) {
2838 $copy->status( OILS_COPY_STATUS_RESHELVING );
2840 $self->checkin_changed(1);
2845 # Returns true if the item is at the current location
2846 # because it was transited there for a hold and the
2847 # hold has not been fulfilled
2848 sub checkin_check_holds_shelf {
2850 return 0 unless $self->copy;
2853 $U->copy_status($self->copy->status)->id ==
2854 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2856 # Attempt to clear shelf expired holds for this copy
2857 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2858 if($self->clear_expired);
2860 # find the hold that put us on the holds shelf
2861 my $holds = $self->editor->search_action_hold_request(
2863 current_copy => $self->copy->id,
2864 capture_time => { '!=' => undef },
2865 fulfillment_time => undef,
2866 cancel_time => undef,
2871 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2872 $self->reshelve_copy(1);
2876 my $hold = $$holds[0];
2878 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2879 $hold->id. "] for copy ".$self->copy->barcode);
2881 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2882 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2883 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2884 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2885 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2886 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2887 $self->fake_hold_dest(1);
2893 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2894 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2898 $logger->info("circulator: hold is not for here..");
2899 $self->remote_hold($hold);
2904 sub checkin_handle_precat {
2906 my $copy = $self->copy;
2908 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2909 $copy->status(OILS_COPY_STATUS_CATALOGING);
2910 $self->update_copy();
2911 $self->checkin_changed(1);
2912 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2917 sub checkin_build_copy_transit {
2920 my $copy = $self->copy;
2921 my $transit = Fieldmapper::action::transit_copy->new;
2923 # if we are transiting an item to the shelf shelf, it's a hold transit
2924 if (my $hold = $self->remote_hold) {
2925 $transit = Fieldmapper::action::hold_transit_copy->new;
2926 $transit->hold($hold->id);
2928 # the item is going into transit, remove any shelf-iness
2929 if ($hold->current_shelf_lib or $hold->shelf_time) {
2930 $hold->clear_current_shelf_lib;
2931 $hold->clear_shelf_time;
2932 return $self->bail_on_events($self->editor->event)
2933 unless $self->editor->update_action_hold_request($hold);
2937 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2938 $logger->info("circulator: transiting copy to $dest");
2940 $transit->source($self->circ_lib);
2941 $transit->dest($dest);
2942 $transit->target_copy($copy->id);
2943 $transit->source_send_time('now');
2944 $transit->copy_status( $U->copy_status($copy->status)->id );
2946 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2948 if ($self->remote_hold) {
2949 return $self->bail_on_events($self->editor->event)
2950 unless $self->editor->create_action_hold_transit_copy($transit);
2952 return $self->bail_on_events($self->editor->event)
2953 unless $self->editor->create_action_transit_copy($transit);
2956 # ensure the transit is returned to the caller
2957 $self->transit($transit);
2959 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2961 $self->checkin_changed(1);
2965 sub hold_capture_is_possible {
2967 my $copy = $self->copy;
2969 # we've been explicitly told not to capture any holds
2970 return 0 if $self->capture eq 'nocapture';
2972 # See if this copy can fulfill any holds
2973 my $hold = $holdcode->find_nearest_permitted_hold(
2974 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2976 return undef if ref $hold eq "HASH" and
2977 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2981 sub reservation_capture_is_possible {
2983 my $copy = $self->copy;
2985 # we've been explicitly told not to capture any holds
2986 return 0 if $self->capture eq 'nocapture';
2988 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2989 my $resv = $booking_ses->request(
2990 "open-ils.booking.reservations.could_capture",
2991 $self->editor->authtoken, $copy->barcode
2993 $booking_ses->disconnect;
2994 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2995 $self->push_events($resv);
3001 # returns true if the item was used (or may potentially be used
3002 # in subsequent calls) to capture a hold.
3003 sub attempt_checkin_hold_capture {
3005 my $copy = $self->copy;
3007 # we've been explicitly told not to capture any holds
3008 return 0 if $self->capture eq 'nocapture';
3010 # See if this copy can fulfill any holds
3011 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3012 $self->editor, $copy, $self->editor->requestor );
3015 $logger->debug("circulator: no potential permitted".
3016 "holds found for copy ".$copy->barcode);
3020 if($self->capture ne 'capture') {
3021 # see if this item is in a hold-capture-delay location
3022 my $location = $self->copy->location;
3023 if(!ref($location)) {
3024 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3025 $self->copy->location($location);
3027 if($U->is_true($location->hold_verify)) {
3028 $self->bail_on_events(
3029 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3034 $self->retarget($retarget);
3036 my $suppress_transit = 0;
3037 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3038 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3039 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3040 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3041 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3042 $suppress_transit = 1;
3043 $hold->pickup_lib($self->circ_lib);
3048 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3050 $hold->current_copy($copy->id);
3051 $hold->capture_time('now');
3052 $self->put_hold_on_shelf($hold)
3053 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3055 # prevent DB errors caused by fetching
3056 # holds from storage, and updating through cstore
3057 $hold->clear_fulfillment_time;
3058 $hold->clear_fulfillment_staff;
3059 $hold->clear_fulfillment_lib;
3060 $hold->clear_expire_time;
3061 $hold->clear_cancel_time;
3062 $hold->clear_prev_check_time unless $hold->prev_check_time;
3064 $self->bail_on_events($self->editor->event)
3065 unless $self->editor->update_action_hold_request($hold);
3067 $self->checkin_changed(1);
3069 return 0 if $self->bail_out;
3071 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3073 if ($hold->hold_type eq 'R') {
3074 $copy->status(OILS_COPY_STATUS_CATALOGING);
3075 $hold->fulfillment_time('now');
3076 $self->noop(1); # Block other transit/hold checks
3077 $self->bail_on_events($self->editor->event)
3078 unless $self->editor->update_action_hold_request($hold);
3080 # This hold was captured in the correct location
3081 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3082 $self->push_events(OpenILS::Event->new('SUCCESS'));
3084 #$self->do_hold_notify($hold->id);
3085 $self->notify_hold($hold->id);
3090 # Hold needs to be picked up elsewhere. Build a hold
3091 # transit and route the item.
3092 $self->checkin_build_hold_transit();
3093 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3094 return 0 if $self->bail_out;
3095 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3098 # make sure we save the copy status
3100 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3104 sub attempt_checkin_reservation_capture {
3106 my $copy = $self->copy;
3108 # we've been explicitly told not to capture any holds
3109 return 0 if $self->capture eq 'nocapture';
3111 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3112 my $evt = $booking_ses->request(
3113 "open-ils.booking.resources.capture_for_reservation",
3114 $self->editor->authtoken,
3116 1 # don't update copy - we probably have it locked
3118 $booking_ses->disconnect;
3120 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3122 "open-ils.booking.resources.capture_for_reservation " .
3123 "didn't return an event!"
3127 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3128 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3130 # not-transferable is an error event we'll pass on the user
3131 $logger->warn("reservation capture attempted against non-transferable item");
3132 $self->push_events($evt);
3134 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3135 # Re-retrieve copy as reservation capture may have changed
3136 # its status and whatnot.
3138 "circulator: booking capture win on copy " . $self->copy->id
3140 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3142 "circulator: changing copy " . $self->copy->id .
3143 "'s status from " . $self->copy->status . " to " .
3146 $self->copy->status($new_copy_status);
3149 $self->reservation($evt->{"payload"}->{"reservation"});
3151 if (exists $evt->{"payload"}->{"transit"}) {
3155 "org" => $evt->{"payload"}->{"transit"}->dest
3159 $self->checkin_changed(1);
3163 # other results are treated as "nothing to capture"
3167 sub do_hold_notify {
3168 my( $self, $holdid ) = @_;
3170 my $e = new_editor(xact => 1);
3171 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3173 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3174 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3176 $logger->info("circulator: running delayed hold notify process");
3178 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3179 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3181 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3182 hold_id => $holdid, requestor => $self->editor->requestor);
3184 $logger->debug("circulator: built hold notifier");
3186 if(!$notifier->event) {
3188 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3190 my $stat = $notifier->send_email_notify;
3191 if( $stat == '1' ) {
3192 $logger->info("circulator: hold notify succeeded for hold $holdid");
3196 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3199 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3203 sub retarget_holds {
3205 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3206 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3207 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3208 # no reason to wait for the return value
3212 sub checkin_build_hold_transit {
3215 my $copy = $self->copy;
3216 my $hold = $self->hold;
3217 my $trans = Fieldmapper::action::hold_transit_copy->new;
3219 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3221 $trans->hold($hold->id);
3222 $trans->source($self->circ_lib);
3223 $trans->dest($hold->pickup_lib);
3224 $trans->source_send_time("now");
3225 $trans->target_copy($copy->id);
3227 # when the copy gets to its destination, it will recover
3228 # this status - put it onto the holds shelf
3229 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3231 return $self->bail_on_events($self->editor->event)
3232 unless $self->editor->create_action_hold_transit_copy($trans);
3237 sub process_received_transit {
3239 my $copy = $self->copy;
3240 my $copyid = $self->copy->id;
3242 my $status_name = $U->copy_status($copy->status)->name;
3243 $logger->debug("circulator: attempting transit receive on ".
3244 "copy $copyid. Copy status is $status_name");
3246 my $transit = $self->transit;
3248 # Check if we are in a transit suppress range
3249 my $suppress_transit = 0;
3250 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3251 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3252 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3253 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3254 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3255 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3256 $suppress_transit = 1;
3257 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3261 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3262 # - this item is in-transit to a different location
3263 # - Or we are capturing holds as transits, so why create a new transit?
3265 my $tid = $transit->id;
3266 my $loc = $self->circ_lib;
3267 my $dest = $transit->dest;
3269 $logger->info("circulator: Fowarding transit on copy which is destined ".
3270 "for a different location. transit=$tid, copy=$copyid, current ".
3271 "location=$loc, destination location=$dest");
3273 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3275 # grab the associated hold object if available
3276 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3277 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3279 return $self->bail_on_events($evt);
3282 # The transit is received, set the receive time
3283 $transit->dest_recv_time('now');
3284 $self->bail_on_events($self->editor->event)
3285 unless $self->editor->update_action_transit_copy($transit);
3287 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3289 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3290 $copy->status( $transit->copy_status );
3291 $self->update_copy();
3292 return if $self->bail_out;
3296 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3299 # hold has arrived at destination, set shelf time
3300 $self->put_hold_on_shelf($hold);
3301 $self->bail_on_events($self->editor->event)
3302 unless $self->editor->update_action_hold_request($hold);
3303 return if $self->bail_out;
3305 $self->notify_hold($hold_transit->hold);
3308 $hold_transit = undef;
3309 $self->cancelled_hold_transit(1);
3310 $self->reshelve_copy(1);
3311 $self->fake_hold_dest(0);
3316 OpenILS::Event->new(
3319 payload => { transit => $transit, holdtransit => $hold_transit } ));
3321 return $hold_transit;
3325 # ------------------------------------------------------------------
3326 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3327 # ------------------------------------------------------------------
3328 sub put_hold_on_shelf {
3329 my($self, $hold) = @_;
3330 $hold->shelf_time('now');
3331 $hold->current_shelf_lib($self->circ_lib);
3332 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3340 my $reservation = shift;
3341 my $ignore_stop_fines = shift;
3342 my $dt_parser = DateTime::Format::ISO8601->new;
3344 my $obj = $reservation ? $self->reservation : $self->circ;
3346 return undef if (!$ignore_stop_fines and $obj->stop_fines);
3348 # If we have a grace period
3349 if($obj->can('grace_period')) {
3350 # Parse out the due date
3351 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3352 # Add the grace period to the due date
3353 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3354 # Don't generate fines on circs still in grace period
3355 return undef if ($due_date > DateTime->now);
3358 $CC->generate_fines({circs => [$obj], editor => $self->editor});
3363 sub checkin_handle_circ {
3365 my $circ = $self->circ;
3366 my $copy = $self->copy;
3370 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3372 # backdate the circ if necessary
3373 if($self->backdate) {
3374 my $evt = $self->checkin_handle_backdate;
3375 return $self->bail_on_events($evt) if $evt;
3378 if(!$circ->stop_fines) {
3379 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3380 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3381 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3382 $circ->stop_fines_time('now');
3383 $circ->stop_fines_time($self->backdate) if $self->backdate;
3386 # Set the checkin vars since we have the item
3387 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3389 # capture the true scan time for back-dated checkins
3390 $circ->checkin_scan_time('now');
3392 $circ->checkin_staff($self->editor->requestor->id);
3393 $circ->checkin_lib($self->circ_lib);
3394 $circ->checkin_workstation($self->editor->requestor->wsid);
3396 my $circ_lib = (ref $self->copy->circ_lib) ?
3397 $self->copy->circ_lib->id : $self->copy->circ_lib;
3398 my $stat = $U->copy_status($self->copy->status)->id;
3400 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3401 # we will now handle lost fines, but the copy will retain its 'lost'
3402 # status if it needs to transit home unless lost_immediately_available
3405 # if we decide to also delay fine handling until the item arrives home,
3406 # we will need to call lost fine handling code both when checking items
3407 # in and also when receiving transits
3408 $self->checkin_handle_lost($circ_lib);
3409 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3410 # same process as above.
3411 $self->checkin_handle_long_overdue($circ_lib);
3412 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3413 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3415 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3420 # see if there are any fines owed on this circ. if not, close it
3421 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3422 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3424 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3426 return $self->bail_on_events($self->editor->event)
3427 unless $self->editor->update_action_circulation($circ);
3432 # ------------------------------------------------------------------
3433 # See if we need to void billings, etc. for lost checkin
3434 # ------------------------------------------------------------------
3435 sub checkin_handle_lost {
3437 my $circ_lib = shift;
3439 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3440 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3442 return $self->checkin_handle_lost_or_longoverdue(
3443 circ_lib => $circ_lib,
3444 max_return => $max_return,
3445 ous_dont_change_on_zero => 'circ.checkin.lost_zero_balance.do_not_change',
3446 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3447 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3448 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3449 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3450 ous_use_last_activity => undef, # not supported for LOST checkin
3451 void_cost_btype => 3,
3456 # ------------------------------------------------------------------
3457 # See if we need to void billings, etc. for long-overdue checkin
3458 # note: not using constants below since they serve little purpose
3459 # for single-use strings that are descriptive in their own right
3460 # and mostly just complicate debugging.
3461 # ------------------------------------------------------------------
3462 sub checkin_handle_long_overdue {
3464 my $circ_lib = shift;
3466 $logger->info("circulator: processing long-overdue checkin...");
3468 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3469 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3471 return $self->checkin_handle_lost_or_longoverdue(
3472 circ_lib => $circ_lib,
3473 max_return => $max_return,
3474 is_longoverdue => 1,
3475 ous_dont_change_on_zero => 'circ.checkin.lost_zero_balance.do_not_change',
3476 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3477 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3478 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3479 ous_immediately_available => 'circ.longoverdue_immediately_available',
3480 ous_use_last_activity =>
3481 'circ.longoverdue.use_last_activity_date_on_return',
3482 void_cost_btype => 10,
3483 void_fee_btype => 11
3487 # last billing activity is last payment time, last billing time, or the
3488 # circ due date. If the relevant "use last activity" org unit setting is
3489 # false/unset, then last billing activity is always the due date.
3490 sub get_circ_last_billing_activity {
3492 my $circ_lib = shift;
3493 my $setting = shift;
3494 my $date = $self->circ->due_date;
3496 return $date unless $setting and
3497 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3499 my $xact = $self->editor->retrieve_money_billable_transaction([
3501 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3504 if ($xact->summary) {
3505 $date = $xact->summary->last_payment_ts ||
3506 $xact->summary->last_billing_ts ||
3507 $self->circ->due_date;
3514 sub checkin_handle_lost_or_longoverdue {
3515 my ($self, %args) = @_;
3517 my $circ = $self->circ;
3518 my $max_return = $args{max_return};
3519 my $circ_lib = $args{circ_lib};
3524 $self->get_circ_last_billing_activity(
3525 $circ_lib, $args{ous_use_last_activity});
3528 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3529 $tm[5] -= 1 if $tm[5] > 0;
3530 my $due = timelocal(int($tm[1]), int($tm[2]),
3531 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3534 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3536 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3537 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3538 "DUE: $due LAST: $last_chance");
3540 $max_return = 0 if $today < $last_chance;
3546 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3547 "return interval. skipping fine/fee voiding, etc.");
3549 } else { # within max-return interval or no interval defined
3551 $logger->info("circulator: check-in of lost/lo item is within the ".
3552 "max return interval (or no interval is defined). Proceeding ".
3553 "with fine/fee voiding, etc.");
3555 my $dont_change = $U->ou_ancestor_setting_value(
3556 $circ_lib, $args{ous_dont_change_on_zero}, $self->editor) || 0;
3559 my ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3560 $dont_change = 0 if( $obt and $obt->balance_owed != 0 );
3563 $logger->info("circulator: check-in of lost/lo item having a balance ".
3564 "of zero, skipping fine/fee voiding and reinstatement.") if ($dont_change);
3566 my $void_cost = $U->ou_ancestor_setting_value(
3567 $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3568 my $void_proc_fee = $U->ou_ancestor_setting_value(
3569 $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3570 my $restore_od = $U->ou_ancestor_setting_value(
3571 $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3573 $self->checkin_handle_lost_or_lo_now_found(
3574 $args{void_cost_btype}, $args{is_longoverdue}) if ($void_cost and !$dont_change);
3575 $self->checkin_handle_lost_or_lo_now_found(
3576 $args{void_fee_btype}, $args{is_longoverdue}) if ($void_proc_fee and !$dont_change);
3577 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3578 if ! $dont_change && $restore_od && ! $self->void_overdues;
3581 if ($circ_lib != $self->circ_lib) {
3582 # if the item is not home, check to see if we want to retain the
3583 # lost/longoverdue status at this point in the process
3585 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3586 $args{ous_immediately_available}, $self->editor) || 0;
3588 if ($immediately_available) {
3589 # item status does not need to be retained, so give it a
3590 # reshelving status as if it were a normal checkin
3591 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3594 $logger->info("circulator: leaving lost/longoverdue copy".
3595 " status in place on checkin");
3598 # lost/longoverdue item is home and processed, treat like a normal
3599 # checkin from this point on
3600 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3606 sub checkin_handle_backdate {
3609 # ------------------------------------------------------------------
3610 # clean up the backdate for date comparison
3611 # XXX We are currently taking the due-time from the original due-date,
3612 # not the input. Do we need to do this? This certainly interferes with
3613 # backdating of hourly checkouts, but that is likely a very rare case.
3614 # ------------------------------------------------------------------
3615 my $bd = cleanse_ISO8601($self->backdate);
3616 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3617 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3618 $new_date->set_hour($original_date->hour());
3619 $new_date->set_minute($original_date->minute());
3620 if ($new_date >= DateTime->now) {
3621 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3624 $bd = cleanse_ISO8601($new_date->datetime());
3627 $self->backdate($bd);
3632 sub check_checkin_copy_status {
3634 my $copy = $self->copy;
3636 my $status = $U->copy_status($copy->status)->id;
3639 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3640 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3641 $status == OILS_COPY_STATUS_IN_PROCESS ||
3642 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3643 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3644 $status == OILS_COPY_STATUS_CATALOGING ||
3645 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3646 $status == OILS_COPY_STATUS_RESHELVING );
3648 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3649 if( $status == OILS_COPY_STATUS_LOST );
3651 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3652 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3654 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3655 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3657 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3658 if( $status == OILS_COPY_STATUS_MISSING );
3660 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3665 # --------------------------------------------------------------------------
3666 # On checkin, we need to return as many relevant objects as we can
3667 # --------------------------------------------------------------------------
3668 sub checkin_flesh_events {
3671 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3672 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3673 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3676 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3679 if($self->hold and !$self->hold->cancel_time) {
3680 $hold = $self->hold;
3681 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3685 # update our copy of the circ object and
3686 # flesh the billing summary data
3688 $self->editor->retrieve_action_circulation([
3692 circ => ['billable_transaction'],
3701 # flesh some patron fields before returning
3703 $self->editor->retrieve_actor_user([
3708 au => ['card', 'billing_address', 'mailing_address']
3715 for my $evt (@{$self->events}) {
3718 $payload->{copy} = $U->unflesh_copy($self->copy);
3719 $payload->{volume} = $self->volume;
3720 $payload->{record} = $record,
3721 $payload->{circ} = $self->circ;
3722 $payload->{transit} = $self->transit;
3723 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3724 $payload->{hold} = $hold;
3725 $payload->{patron} = $self->patron;
3726 $payload->{reservation} = $self->reservation
3727 unless (not $self->reservation or $self->reservation->cancel_time);
3729 $evt->{payload} = $payload;
3734 my( $self, $msg ) = @_;
3735 my $bc = ($self->copy) ? $self->copy->barcode :
3738 my $usr = ($self->patron) ? $self->patron->id : "";
3739 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3740 ", recipient=$usr, copy=$bc");
3746 $self->log_me("do_renew()");
3748 # Make sure there is an open circ to renew that is not
3749 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3750 my $usrid = $self->patron->id if $self->patron;
3751 my $circ = $self->editor->search_action_circulation({
3752 target_copy => $self->copy->id,
3753 xact_finish => undef,
3754 checkin_time => undef,
3755 ($usrid ? (usr => $usrid) : ()),
3757 {stop_fines => undef},
3758 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3762 return $self->bail_on_events($self->editor->event) unless $circ;
3764 # A user is not allowed to renew another user's items without permission
3765 unless( $circ->usr eq $self->editor->requestor->id ) {
3766 return $self->bail_on_events($self->editor->events)
3767 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3770 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3771 if $circ->renewal_remaining < 1;
3773 # -----------------------------------------------------------------
3775 $self->parent_circ($circ->id);
3776 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3779 # Opac renewal - re-use circ library from original circ (unless told not to)
3780 if($self->opac_renewal) {
3781 unless(defined($opac_renewal_use_circ_lib)) {
3782 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3783 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3784 $opac_renewal_use_circ_lib = 1;
3787 $opac_renewal_use_circ_lib = 0;
3790 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3793 # Desk renewal - re-use circ library from original circ (unless told not to)
3794 if($self->desk_renewal) {
3795 unless(defined($desk_renewal_use_circ_lib)) {
3796 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3797 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3798 $desk_renewal_use_circ_lib = 1;
3801 $desk_renewal_use_circ_lib = 0;
3804 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3807 # Run the fine generator against the old circ
3808 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3809 # a few lines down. Commenting out, for now.
3810 #$self->handle_fines;
3812 $self->run_renew_permit;
3815 $self->do_checkin();
3816 return if $self->bail_out;
3818 unless( $self->permit_override ) {
3820 return if $self->bail_out;
3821 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3822 $self->remove_event('ITEM_NOT_CATALOGED');
3825 $self->override_events;
3826 return if $self->bail_out;
3829 $self->do_checkout();
3834 my( $self, $evt ) = @_;
3835 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3836 $logger->debug("circulator: removing event from list: $evt");
3837 my @events = @{$self->events};
3838 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3843 my( $self, $evt ) = @_;
3844 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3845 return grep { $_->{textcode} eq $evt } @{$self->events};
3850 sub run_renew_permit {
3853 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3854 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3855 $self->editor, $self->copy, $self->editor->requestor, 1
3857 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3860 if(!$self->legacy_script_support) {
3861 my $results = $self->run_indb_circ_test;
3862 $self->push_events($self->matrix_test_result_events)
3863 unless $self->circ_test_success;
3866 my $runner = $self->script_runner;
3868 $runner->load($self->circ_permit_renew);
3869 my $result = $runner->run or
3870 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3871 if ($result->{"events"}) {
3873 map { new OpenILS::Event($_) } @{$result->{"events"}}
3876 "circulator: circ_permit_renew for user " .
3877 $self->patron->id . " returned " .
3878 scalar(@{$result->{"events"}}) . " event(s)"
3882 $self->mk_script_runner;
3885 $logger->debug("circulator: re-creating script runner to be safe");
3889 # XXX: The primary mechanism for storing circ history is now handled
3890 # by tracking real circulation objects instead of bibs in a bucket.
3891 # However, this code is disabled by default and could be useful
3892 # some day, so may as well leave it for now.
3893 sub append_reading_list {
3897 $self->is_checkout and
3903 # verify history is globally enabled and uses the bucket mechanism
3904 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3905 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3907 return undef unless $htype and $htype eq 'bucket';
3909 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3911 # verify the patron wants to retain the hisory
3912 my $setting = $e->search_actor_user_setting(
3913 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3915 unless($setting and $setting->value) {
3920 my $bkt = $e->search_container_copy_bucket(
3921 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3926 # find the next item position
3927 my $last_item = $e->search_container_copy_bucket_item(
3928 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3929 $pos = $last_item->pos + 1 if $last_item;
3932 # create the history bucket if necessary
3933 $bkt = Fieldmapper::container::copy_bucket->new;
3934 $bkt->owner($self->patron->id);
3936 $bkt->btype('circ_history');
3938 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3941 my $item = Fieldmapper::container::copy_bucket_item->new;
3943 $item->bucket($bkt->id);
3944 $item->target_copy($self->copy->id);
3947 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3954 sub make_trigger_events {
3956 return unless $self->circ;
3957 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3958 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3959 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3964 sub checkin_handle_lost_or_lo_now_found {
3965 my ($self, $bill_type, $is_longoverdue) = @_;
3967 # ------------------------------------------------------------------
3968 # remove charge from patron's account if lost item is returned
3969 # ------------------------------------------------------------------
3971 my $bills = $self->editor->search_money_billing(
3973 xact => $self->circ->id,
3978 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3980 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
3981 for my $bill (@$bills) {
3982 if( !$U->is_true($bill->voided) ) {
3983 $logger->info("$tag item returned - voiding bill ".$bill->id);
3985 $bill->void_time('now');
3986 $bill->voider($self->editor->requestor->id);
3987 my $note = ($bill->note) ? $bill->note . "\n" : '';
3988 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
3990 $self->bail_on_events($self->editor->event)
3991 unless $self->editor->update_money_billing($bill);
3996 sub checkin_handle_lost_or_lo_now_found_restore_od {
3998 my $circ_lib = shift;
3999 my $is_longoverdue = shift;
4000 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4002 # ------------------------------------------------------------------
4003 # restore those overdue charges voided when item was set to lost
4004 # ------------------------------------------------------------------
4006 my $ods = $self->editor->search_money_billing(
4008 xact => $self->circ->id,
4013 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4014 for my $bill (@$ods) {
4015 if( $U->is_true($bill->voided) ) {
4016 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4018 $bill->clear_void_time;
4019 $bill->voider($self->editor->requestor->id);
4020 my $note = ($bill->note) ? $bill->note . "\n" : '';
4021 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4023 $self->bail_on_events($self->editor->event)
4024 unless $self->editor->update_money_billing($bill);