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 $holdcode = "OpenILS::Application::Circ::Holds";
438 my $transcode = "OpenILS::Application::Circ::Transit";
444 # --------------------------------------------------------------------------
445 # Add a pile of automagic getter/setter methods
446 # --------------------------------------------------------------------------
447 my @AUTOLOAD_FIELDS = qw/
494 recurring_fines_level
507 cancelled_hold_transit
514 circ_matrix_matchpoint
516 legacy_script_support
526 claims_never_checked_out
544 my $type = ref($self) or die "$self is not an object";
546 my $name = $AUTOLOAD;
549 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
550 $logger->error("circulator: $type: invalid autoload field: $name");
551 die "$type: invalid autoload field: $name\n"
556 *{"${type}::${name}"} = sub {
559 $s->{$name} = $v if defined $v;
563 return $self->$name($data);
568 my( $class, $auth, %args ) = @_;
569 $class = ref($class) || $class;
570 my $self = bless( {}, $class );
573 $self->editor(new_editor(xact => 1, authtoken => $auth));
575 unless( $self->editor->checkauth ) {
576 $self->bail_on_events($self->editor->event);
580 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
582 $self->$_($args{$_}) for keys %args;
585 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
587 # if this is a renewal, default to desk_renewal
588 $self->desk_renewal(1) unless
589 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
591 $self->capture('') unless $self->capture;
593 unless(%user_groups) {
594 my $gps = $self->editor->retrieve_all_permission_grp_tree;
595 %user_groups = map { $_->id => $_ } @$gps;
602 # --------------------------------------------------------------------------
603 # True if we should discontinue processing
604 # --------------------------------------------------------------------------
606 my( $self, $bool ) = @_;
607 if( defined $bool ) {
608 $logger->info("circulator: BAILING OUT") if $bool;
609 $self->{bail_out} = $bool;
611 return $self->{bail_out};
616 my( $self, @evts ) = @_;
619 $e->{payload} = $self->copy if
620 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
622 $logger->info("circulator: pushing event ".$e->{textcode});
623 push( @{$self->events}, $e ) unless
624 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
630 return '' if $self->skip_permit_key;
631 my $key = md5_hex( time() . rand() . "$$" );
632 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
633 return $self->permit_key($key);
636 sub check_permit_key {
638 return 1 if $self->skip_permit_key;
639 my $key = $self->permit_key;
640 return 0 unless $key;
641 my $k = "oils_permit_key_$key";
642 my $one = $self->cache_handle->get_cache($k);
643 $self->cache_handle->delete_cache($k);
644 return ($one) ? 1 : 0;
647 sub seems_like_reservation {
650 # Some words about the following method:
651 # 1) It requires the VIEW_USER permission, but that's not an
652 # issue, right, since all staff should have that?
653 # 2) It returns only one reservation at a time, even if an item can be
654 # and is currently overbooked. Hmmm....
655 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
656 my $result = $booking_ses->request(
657 "open-ils.booking.reservations.by_returnable_resource_barcode",
658 $self->editor->authtoken,
661 $booking_ses->disconnect;
663 return $self->bail_on_events($result) if defined $U->event_code($result);
666 $self->reservation(shift @$result);
674 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
675 sub save_trimmed_copy {
676 my ($self, $copy) = @_;
679 $self->volume($copy->call_number);
680 $self->title($self->volume->record);
681 $self->copy->call_number($self->volume->id);
682 $self->volume->record($self->title->id);
683 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
684 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
685 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
686 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
692 my $e = $self->editor;
694 # --------------------------------------------------------------------------
695 # Grab the fleshed copy
696 # --------------------------------------------------------------------------
697 unless($self->is_noncat) {
700 $copy = $e->retrieve_asset_copy(
701 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
703 } elsif( $self->copy_barcode ) {
705 $copy = $e->search_asset_copy(
706 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
707 } elsif( $self->reservation ) {
708 my $res = $e->json_query(
710 "select" => {"acp" => ["id"]},
715 "field" => "barcode",
719 "field" => "current_resource"
727 "id" => (ref $self->reservation) ?
728 $self->reservation->id : $self->reservation
733 if (ref $res eq "ARRAY" and scalar @$res) {
734 $logger->info("circulator: mapped reservation " .
735 $self->reservation . " to copy " . $res->[0]->{"id"});
736 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
741 $self->save_trimmed_copy($copy);
743 # We can't renew if there is no copy
744 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
745 if $self->is_renewal;
750 # --------------------------------------------------------------------------
752 # --------------------------------------------------------------------------
756 flesh_fields => {au => [ qw/ card / ]}
759 if( $self->patron_id ) {
760 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
761 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
763 } elsif( $self->patron_barcode ) {
765 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
766 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
767 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
769 $patron = $e->retrieve_actor_user($card->usr)
770 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
772 # Use the card we looked up, not the patron's primary, for card active checks
773 $patron->card($card);
776 if( my $copy = $self->copy ) {
779 $flesh->{flesh_fields}->{circ} = ['usr'];
781 my $circ = $e->search_action_circulation([
782 {target_copy => $copy->id, checkin_time => undef}, $flesh
786 $patron = $circ->usr;
787 $circ->usr($patron->id); # de-flesh for consistency
793 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
794 unless $self->patron($patron) or $self->is_checkin;
796 unless($self->is_checkin) {
798 # Check for inactivity and patron reg. expiration
800 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
801 unless $U->is_true($patron->active);
803 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
804 unless $U->is_true($patron->card->active);
806 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
807 cleanse_ISO8601($patron->expire_date));
809 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
810 if( CORE::time > $expire->epoch ) ;
814 # --------------------------------------------------------------------------
815 # This builds the script runner environment and fetches most of the
817 # --------------------------------------------------------------------------
818 sub mk_script_runner {
824 qw/copy copy_barcode copy_id patron
825 patron_id patron_barcode volume title editor/;
827 # Translate our objects into the ScriptBuilder args hash
828 $$args{$_} = $self->$_() for @fields;
830 $args->{ignore_user_status} = 1 if $self->is_checkin;
831 $$args{fetch_patron_by_circ_copy} = 1;
832 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
834 if( my $pco = $self->pending_checkouts ) {
835 $logger->info("circulator: we were given a pending checkouts number of $pco");
836 $$args{patronItemsOut} = $pco;
839 # This fetches most of the objects we need
840 $self->script_runner(
841 OpenILS::Application::Circ::ScriptBuilder->build($args));
843 # Now we translate the ScriptBuilder objects back into self
844 $self->$_($$args{$_}) for @fields;
846 my @evts = @{$args->{_events}} if $args->{_events};
848 $logger->debug("circulator: script builder returned events: @evts") if @evts;
852 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
853 if(!$self->is_noncat and
855 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
859 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
860 return $self->bail_on_events(@e);
865 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
866 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
867 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
868 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
872 # We can't renew if there is no copy
873 return $self->bail_on_events(@evts) if
874 $self->is_renewal and !$self->copy;
876 # Set some circ-specific flags in the script environment
877 my $evt = "environment";
878 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
880 if( $self->is_noncat ) {
881 $self->script_runner->insert("$evt.isNonCat", 1);
882 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
885 if( $self->is_precat ) {
886 $self->script_runner->insert("environment.isPrecat", 1, 1);
889 $self->script_runner->add_path( $_ ) for @$script_libs;
894 # --------------------------------------------------------------------------
895 # Does the circ permit work
896 # --------------------------------------------------------------------------
900 $self->log_me("do_permit()");
902 unless( $self->editor->requestor->id == $self->patron->id ) {
903 return $self->bail_on_events($self->editor->event)
904 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
907 $self->check_captured_holds();
908 $self->do_copy_checks();
909 return if $self->bail_out;
910 $self->run_patron_permit_scripts();
911 $self->run_copy_permit_scripts()
912 unless $self->is_precat or $self->is_noncat;
913 $self->check_item_deposit_events();
914 $self->override_events();
915 return if $self->bail_out;
917 if($self->is_precat and not $self->request_precat) {
920 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
921 return $self->bail_out(1) unless $self->is_renewal;
925 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
928 sub check_item_deposit_events {
930 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
931 if $self->is_deposit and not $self->is_deposit_exempt;
932 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
933 if $self->is_rental and not $self->is_rental_exempt;
936 # returns true if the user is not required to pay deposits
937 sub is_deposit_exempt {
939 my $pid = (ref $self->patron->profile) ?
940 $self->patron->profile->id : $self->patron->profile;
941 my $groups = $U->ou_ancestor_setting_value(
942 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
943 for my $grp (@$groups) {
944 return 1 if $self->is_group_descendant($grp, $pid);
949 # returns true if the user is not required to pay rental fees
950 sub is_rental_exempt {
952 my $pid = (ref $self->patron->profile) ?
953 $self->patron->profile->id : $self->patron->profile;
954 my $groups = $U->ou_ancestor_setting_value(
955 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
956 for my $grp (@$groups) {
957 return 1 if $self->is_group_descendant($grp, $pid);
962 sub is_group_descendant {
963 my($self, $p_id, $c_id) = @_;
964 return 0 unless defined $p_id and defined $c_id;
965 return 1 if $c_id == $p_id;
966 while(my $grp = $user_groups{$c_id}) {
967 $c_id = $grp->parent;
968 return 0 unless defined $c_id;
969 return 1 if $c_id == $p_id;
974 sub check_captured_holds {
976 my $copy = $self->copy;
977 my $patron = $self->patron;
979 return undef unless $copy;
981 my $s = $U->copy_status($copy->status)->id;
982 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
983 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
985 # Item is on the holds shelf, make sure it's going to the right person
986 my $hold = $self->editor->search_action_hold_request(
989 current_copy => $copy->id ,
990 capture_time => { '!=' => undef },
991 cancel_time => undef,
992 fulfillment_time => undef
998 if ($hold and $hold->usr == $patron->id) {
999 $self->checkout_is_for_hold(1);
1003 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1005 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1009 sub do_copy_checks {
1011 my $copy = $self->copy;
1012 return unless $copy;
1014 my $stat = $U->copy_status($copy->status)->id;
1016 # We cannot check out a copy if it is in-transit
1017 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1018 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1021 $self->handle_claims_returned();
1022 return if $self->bail_out;
1024 # no claims returned circ was found, check if there is any open circ
1025 unless( $self->is_renewal ) {
1027 my $circs = $self->editor->search_action_circulation(
1028 { target_copy => $copy->id, checkin_time => undef }
1031 if(my $old_circ = $circs->[0]) { # an open circ was found
1033 my $payload = {copy => $copy};
1035 if($old_circ->usr == $self->patron->id) {
1037 $payload->{old_circ} = $old_circ;
1039 # If there is an open circulation on the checkout item and an auto-renew
1040 # interval is defined, inform the caller that they should go
1041 # ahead and renew the item instead of warning about open circulations.
1043 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1045 'circ.checkout_auto_renew_age',
1049 if($auto_renew_intvl) {
1050 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1051 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1053 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1054 $payload->{auto_renew} = 1;
1059 return $self->bail_on_events(
1060 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1066 my $LEGACY_CIRC_EVENT_MAP = {
1067 'no_item' => 'ITEM_NOT_CATALOGED',
1068 'actor.usr.barred' => 'PATRON_BARRED',
1069 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1070 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1071 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1072 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1073 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1074 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1075 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1076 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1080 # ---------------------------------------------------------------------
1081 # This pushes any patron-related events into the list but does not
1082 # set bail_out for any events
1083 # ---------------------------------------------------------------------
1084 sub run_patron_permit_scripts {
1086 my $runner = $self->script_runner;
1087 my $patronid = $self->patron->id;
1091 if(!$self->legacy_script_support) {
1093 my $results = $self->run_indb_circ_test;
1094 unless($self->circ_test_success) {
1095 my @trimmed_results;
1097 if ($self->is_noncat) {
1098 # no_item result is OK during noncat checkout
1099 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1103 if ($self->checkout_is_for_hold) {
1104 # if this checkout will fulfill a hold, ignore CIRC blocks
1105 # and rely instead on the (later-checked) FULFILL block
1107 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1108 my $fblock_pens = $self->editor->search_config_standing_penalty(
1109 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1111 for my $res (@$results) {
1112 my $name = $res->{fail_part} || '';
1113 next if grep {$_->name eq $name} @$fblock_pens;
1114 push(@trimmed_results, $res);
1118 # not for hold or noncat
1119 @trimmed_results = @$results;
1123 # update the final set of test results
1124 $self->matrix_test_result(\@trimmed_results);
1126 push @allevents, $self->matrix_test_result_events;
1131 # ---------------------------------------------------------------------
1132 # # Now run the patron permit script
1133 # ---------------------------------------------------------------------
1134 $runner->load($self->circ_permit_patron);
1135 my $result = $runner->run or
1136 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1138 my $patron_events = $result->{events};
1140 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1141 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1142 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1143 $penalties = $penalties->{fatal_penalties};
1145 for my $pen (@$penalties) {
1146 # CIRC blocks are ignored if this is a FULFILL scenario
1147 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1148 my $event = OpenILS::Event->new($pen->name);
1149 $event->{desc} = $pen->label;
1150 push(@allevents, $event);
1153 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1157 $_->{payload} = $self->copy if
1158 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1161 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1163 $self->push_events(@allevents);
1166 sub matrix_test_result_codes {
1168 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1171 sub matrix_test_result_events {
1174 my $event = new OpenILS::Event(
1175 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1177 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1179 } (@{$self->matrix_test_result});
1182 sub run_indb_circ_test {
1184 return $self->matrix_test_result if $self->matrix_test_result;
1186 my $dbfunc = ($self->is_renewal) ?
1187 'action.item_user_renew_test' : 'action.item_user_circ_test';
1189 if( $self->is_precat && $self->request_precat) {
1190 $self->make_precat_copy;
1191 return if $self->bail_out;
1194 my $results = $self->editor->json_query(
1198 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1204 $self->circ_test_success($U->is_true($results->[0]->{success}));
1206 if(my $mp = $results->[0]->{matchpoint}) {
1207 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1208 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1209 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1210 if(defined($results->[0]->{renewals})) {
1211 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1213 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1214 if(defined($results->[0]->{grace_period})) {
1215 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1217 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1218 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1219 # Grab the *last* response for limit_groups, where it is more likely to be filled
1220 $self->limit_groups($results->[-1]->{limit_groups});
1223 return $self->matrix_test_result($results);
1226 # ---------------------------------------------------------------------
1227 # given a use and copy, this will calculate the circulation policy
1228 # parameters. Only works with in-db circ.
1229 # ---------------------------------------------------------------------
1233 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1235 $self->run_indb_circ_test;
1238 circ_test_success => $self->circ_test_success,
1239 failure_events => [],
1240 failure_codes => [],
1241 matchpoint => $self->circ_matrix_matchpoint
1244 unless($self->circ_test_success) {
1245 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1246 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1249 if($self->circ_matrix_matchpoint) {
1250 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1251 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1252 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1253 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1255 my $policy = $self->get_circ_policy(
1256 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1258 $$results{$_} = $$policy{$_} for keys %$policy;
1264 # ---------------------------------------------------------------------
1265 # Loads the circ policy info for duration, recurring fine, and max
1266 # fine based on the current copy
1267 # ---------------------------------------------------------------------
1268 sub get_circ_policy {
1269 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1272 duration_rule => $duration_rule->name,
1273 recurring_fine_rule => $recurring_fine_rule->name,
1274 max_fine_rule => $max_fine_rule->name,
1275 max_fine => $self->get_max_fine_amount($max_fine_rule),
1276 fine_interval => $recurring_fine_rule->recurrence_interval,
1277 renewal_remaining => $duration_rule->max_renewals,
1278 grace_period => $recurring_fine_rule->grace_period
1281 if($hard_due_date) {
1282 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1283 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1286 $policy->{duration_date_ceiling} = undef;
1287 $policy->{duration_date_ceiling_force} = undef;
1290 $policy->{duration} = $duration_rule->shrt
1291 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1292 $policy->{duration} = $duration_rule->normal
1293 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1294 $policy->{duration} = $duration_rule->extended
1295 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1297 $policy->{recurring_fine} = $recurring_fine_rule->low
1298 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1299 $policy->{recurring_fine} = $recurring_fine_rule->normal
1300 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1301 $policy->{recurring_fine} = $recurring_fine_rule->high
1302 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1307 sub get_max_fine_amount {
1309 my $max_fine_rule = shift;
1310 my $max_amount = $max_fine_rule->amount;
1312 # if is_percent is true then the max->amount is
1313 # use as a percentage of the copy price
1314 if ($U->is_true($max_fine_rule->is_percent)) {
1315 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1316 $max_amount = $price * $max_fine_rule->amount / 100;
1318 $U->ou_ancestor_setting_value(
1320 'circ.max_fine.cap_at_price',
1324 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1325 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1333 sub run_copy_permit_scripts {
1335 my $copy = $self->copy || return;
1336 my $runner = $self->script_runner;
1340 if(!$self->legacy_script_support) {
1341 my $results = $self->run_indb_circ_test;
1342 push @allevents, $self->matrix_test_result_events
1343 unless $self->circ_test_success;
1346 # ---------------------------------------------------------------------
1347 # Capture all of the copy permit events
1348 # ---------------------------------------------------------------------
1349 $runner->load($self->circ_permit_copy);
1350 my $result = $runner->run or
1351 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1352 my $copy_events = $result->{events};
1354 # ---------------------------------------------------------------------
1355 # Now collect all of the events together
1356 # ---------------------------------------------------------------------
1357 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1360 # See if this copy has an alert message
1361 my $ae = $self->check_copy_alert();
1362 push( @allevents, $ae ) if $ae;
1364 # uniquify the events
1365 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1366 @allevents = values %hash;
1368 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1370 $self->push_events(@allevents);
1374 sub check_copy_alert {
1376 return undef if $self->is_renewal;
1377 return OpenILS::Event->new(
1378 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1379 if $self->copy and $self->copy->alert_message;
1385 # --------------------------------------------------------------------------
1386 # If the call is overriding and has permissions to override every collected
1387 # event, the are cleared. Any event that the caller does not have
1388 # permission to override, will be left in the event list and bail_out will
1390 # XXX We need code in here to cancel any holds/transits on copies
1391 # that are being force-checked out
1392 # --------------------------------------------------------------------------
1393 sub override_events {
1395 my @events = @{$self->events};
1396 return unless @events;
1397 my $oargs = $self->override_args;
1399 if(!$self->override) {
1400 return $self->bail_out(1)
1401 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1406 for my $e (@events) {
1407 my $tc = $e->{textcode};
1408 next if $tc eq 'SUCCESS';
1409 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1410 my $ov = "$tc.override";
1411 $logger->info("circulator: attempting to override event: $ov");
1413 return $self->bail_on_events($self->editor->event)
1414 unless( $self->editor->allowed($ov) );
1416 return $self->bail_out(1);
1422 # --------------------------------------------------------------------------
1423 # If there is an open claimsreturn circ on the requested copy, close the
1424 # circ if overriding, otherwise bail out
1425 # --------------------------------------------------------------------------
1426 sub handle_claims_returned {
1428 my $copy = $self->copy;
1430 my $CR = $self->editor->search_action_circulation(
1432 target_copy => $copy->id,
1433 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1434 checkin_time => undef,
1438 return unless ($CR = $CR->[0]);
1442 # - If the caller has set the override flag, we will check the item in
1443 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1445 $CR->checkin_time('now');
1446 $CR->checkin_scan_time('now');
1447 $CR->checkin_lib($self->circ_lib);
1448 $CR->checkin_workstation($self->editor->requestor->wsid);
1449 $CR->checkin_staff($self->editor->requestor->id);
1451 $evt = $self->editor->event
1452 unless $self->editor->update_action_circulation($CR);
1455 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1458 $self->bail_on_events($evt) if $evt;
1463 # --------------------------------------------------------------------------
1464 # This performs the checkout
1465 # --------------------------------------------------------------------------
1469 $self->log_me("do_checkout()");
1471 # make sure perms are good if this isn't a renewal
1472 unless( $self->is_renewal ) {
1473 return $self->bail_on_events($self->editor->event)
1474 unless( $self->editor->allowed('COPY_CHECKOUT') );
1477 # verify the permit key
1478 unless( $self->check_permit_key ) {
1479 if( $self->permit_override ) {
1480 return $self->bail_on_events($self->editor->event)
1481 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1483 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1487 # if this is a non-cataloged circ, build the circ and finish
1488 if( $self->is_noncat ) {
1489 $self->checkout_noncat;
1491 OpenILS::Event->new('SUCCESS',
1492 payload => { noncat_circ => $self->circ }));
1496 if( $self->is_precat ) {
1497 $self->make_precat_copy;
1498 return if $self->bail_out;
1500 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1501 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1504 $self->do_copy_checks;
1505 return if $self->bail_out;
1507 $self->run_checkout_scripts();
1508 return if $self->bail_out;
1510 $self->build_checkout_circ_object();
1511 return if $self->bail_out;
1513 my $modify_to_start = $self->booking_adjusted_due_date();
1514 return if $self->bail_out;
1516 $self->apply_modified_due_date($modify_to_start);
1517 return if $self->bail_out;
1519 return $self->bail_on_events($self->editor->event)
1520 unless $self->editor->create_action_circulation($self->circ);
1522 # refresh the circ to force local time zone for now
1523 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1525 if($self->limit_groups) {
1526 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1529 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1531 return if $self->bail_out;
1533 $self->apply_deposit_fee();
1534 return if $self->bail_out;
1536 $self->handle_checkout_holds();
1537 return if $self->bail_out;
1539 # ------------------------------------------------------------------------------
1540 # Update the patron penalty info in the DB. Run it for permit-overrides
1541 # since the penalties are not updated during the permit phase
1542 # ------------------------------------------------------------------------------
1543 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1545 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1548 if($self->is_renewal) {
1549 # flesh the billing summary for the checked-in circ
1550 $pcirc = $self->editor->retrieve_action_circulation([
1552 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1557 OpenILS::Event->new('SUCCESS',
1559 copy => $U->unflesh_copy($self->copy),
1560 volume => $self->volume,
1561 circ => $self->circ,
1563 holds_fulfilled => $self->fulfilled_holds,
1564 deposit_billing => $self->deposit_billing,
1565 rental_billing => $self->rental_billing,
1566 parent_circ => $pcirc,
1567 patron => ($self->return_patron) ? $self->patron : undef,
1568 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1574 sub apply_deposit_fee {
1576 my $copy = $self->copy;
1578 ($self->is_deposit and not $self->is_deposit_exempt) or
1579 ($self->is_rental and not $self->is_rental_exempt);
1581 return if $self->is_deposit and $self->skip_deposit_fee;
1582 return if $self->is_rental and $self->skip_rental_fee;
1584 my $bill = Fieldmapper::money::billing->new;
1585 my $amount = $copy->deposit_amount;
1589 if($self->is_deposit) {
1590 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1592 $self->deposit_billing($bill);
1594 $billing_type = OILS_BILLING_TYPE_RENTAL;
1596 $self->rental_billing($bill);
1599 $bill->xact($self->circ->id);
1600 $bill->amount($amount);
1601 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1602 $bill->billing_type($billing_type);
1603 $bill->btype($btype);
1604 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1606 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1611 my $copy = $self->copy;
1613 my $stat = $copy->status if ref $copy->status;
1614 my $loc = $copy->location if ref $copy->location;
1615 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1617 $copy->status($stat->id) if $stat;
1618 $copy->location($loc->id) if $loc;
1619 $copy->circ_lib($circ_lib->id) if $circ_lib;
1620 $copy->editor($self->editor->requestor->id);
1621 $copy->edit_date('now');
1622 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1624 return $self->bail_on_events($self->editor->event)
1625 unless $self->editor->update_asset_copy($self->copy);
1627 $copy->status($U->copy_status($copy->status));
1628 $copy->location($loc) if $loc;
1629 $copy->circ_lib($circ_lib) if $circ_lib;
1632 sub update_reservation {
1634 my $reservation = $self->reservation;
1636 my $usr = $reservation->usr;
1637 my $target_rt = $reservation->target_resource_type;
1638 my $target_r = $reservation->target_resource;
1639 my $current_r = $reservation->current_resource;
1641 $reservation->usr($usr->id) if ref $usr;
1642 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1643 $reservation->target_resource($target_r->id) if ref $target_r;
1644 $reservation->current_resource($current_r->id) if ref $current_r;
1646 return $self->bail_on_events($self->editor->event)
1647 unless $self->editor->update_booking_reservation($self->reservation);
1650 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1651 $self->reservation($reservation);
1655 sub bail_on_events {
1656 my( $self, @evts ) = @_;
1657 $self->push_events(@evts);
1661 # ------------------------------------------------------------------------------
1662 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1663 # affects copies that will fulfill holds and CIRC affects all other copies.
1664 # If blocks exists, bail, push Events onto the event pile, and return true.
1665 # ------------------------------------------------------------------------------
1666 sub check_hold_fulfill_blocks {
1669 # See if the user has any penalties applied that prevent hold fulfillment
1670 my $pens = $self->editor->json_query({
1671 select => {csp => ['name', 'label']},
1672 from => {ausp => {csp => {}}},
1675 usr => $self->patron->id,
1676 org_unit => $U->get_org_full_path($self->circ_lib),
1678 {stop_date => undef},
1679 {stop_date => {'>' => 'now'}}
1682 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1686 return 0 unless @$pens;
1688 for my $pen (@$pens) {
1689 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1690 my $event = OpenILS::Event->new($pen->{name});
1691 $event->{desc} = $pen->{label};
1692 $self->push_events($event);
1695 $self->override_events;
1696 return $self->bail_out;
1700 # ------------------------------------------------------------------------------
1701 # When an item is checked out, see if we can fulfill a hold for this patron
1702 # ------------------------------------------------------------------------------
1703 sub handle_checkout_holds {
1705 my $copy = $self->copy;
1706 my $patron = $self->patron;
1708 my $e = $self->editor;
1709 $self->fulfilled_holds([]);
1711 # non-cats can't fulfill a hold
1712 return if $self->is_noncat;
1714 my $hold = $e->search_action_hold_request({
1715 current_copy => $copy->id ,
1716 cancel_time => undef,
1717 fulfillment_time => undef,
1719 {expire_time => undef},
1720 {expire_time => {'>' => 'now'}}
1724 if($hold and $hold->usr != $patron->id) {
1725 # reset the hold since the copy is now checked out
1727 $logger->info("circulator: un-targeting hold ".$hold->id.
1728 " because copy ".$copy->id." is getting checked out");
1730 $hold->clear_prev_check_time;
1731 $hold->clear_current_copy;
1732 $hold->clear_capture_time;
1733 $hold->clear_shelf_time;
1734 $hold->clear_shelf_expire_time;
1735 $hold->clear_current_shelf_lib;
1737 return $self->bail_on_event($e->event)
1738 unless $e->update_action_hold_request($hold);
1744 $hold = $self->find_related_user_hold($copy, $patron) or return;
1745 $logger->info("circulator: found related hold to fulfill in checkout");
1748 return if $self->check_hold_fulfill_blocks;
1750 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1752 # if the hold was never officially captured, capture it.
1753 $hold->current_copy($copy->id);
1754 $hold->capture_time('now') unless $hold->capture_time;
1755 $hold->fulfillment_time('now');
1756 $hold->fulfillment_staff($e->requestor->id);
1757 $hold->fulfillment_lib($self->circ_lib);
1759 return $self->bail_on_events($e->event)
1760 unless $e->update_action_hold_request($hold);
1762 return $self->fulfilled_holds([$hold->id]);
1766 # ------------------------------------------------------------------------------
1767 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1768 # the patron directly targets the checked out item, see if there is another hold
1769 # for the patron that could be fulfilled by the checked out item. Fulfill the
1770 # oldest hold and only fulfill 1 of them.
1772 # For "another hold":
1774 # First, check for one that the copy matches via hold_copy_map, ensuring that
1775 # *any* hold type that this copy could fill may end up filled.
1777 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1778 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1779 # that are non-requestable to count as capturing those hold types.
1780 # ------------------------------------------------------------------------------
1781 sub find_related_user_hold {
1782 my($self, $copy, $patron) = @_;
1783 my $e = $self->editor;
1785 # holds on precat copies are always copy-level, so this call will
1786 # always return undef. Exit early.
1787 return undef if $self->is_precat;
1789 return undef unless $U->ou_ancestor_setting_value(
1790 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1792 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1794 select => {ahr => ['id']},
1803 fkey => 'current_copy',
1804 type => 'left' # there may be no current_copy
1811 fulfillment_time => undef,
1812 cancel_time => undef,
1814 {expire_time => undef},
1815 {expire_time => {'>' => 'now'}}
1819 target_copy => $self->copy->id
1823 {id => undef}, # left-join copy may be nonexistent
1824 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1828 order_by => {ahr => {request_time => {direction => 'asc'}}},
1832 my $hold_info = $e->json_query($args)->[0];
1833 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1834 return undef if $U->ou_ancestor_setting_value(
1835 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1837 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1839 select => {ahr => ['id']},
1844 fkey => 'current_copy',
1845 type => 'left' # there may be no current_copy
1852 fulfillment_time => undef,
1853 cancel_time => undef,
1855 {expire_time => undef},
1856 {expire_time => {'>' => 'now'}}
1863 target => $self->volume->id
1869 target => $self->title->id
1875 {id => undef}, # left-join copy may be nonexistent
1876 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1880 order_by => {ahr => {request_time => {direction => 'asc'}}},
1884 $hold_info = $e->json_query($args)->[0];
1885 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1890 sub run_checkout_scripts {
1895 my $runner = $self->script_runner;
1904 my $hard_due_date_name;
1906 if(!$self->legacy_script_support) {
1907 $self->run_indb_circ_test();
1908 $duration = $self->circ_matrix_matchpoint->duration_rule;
1909 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1910 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1911 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1915 $runner->load($self->circ_duration);
1917 my $result = $runner->run or
1918 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1920 $duration_name = $result->{durationRule};
1921 $recurring_name = $result->{recurringFinesRule};
1922 $max_fine_name = $result->{maxFine};
1923 $hard_due_date_name = $result->{hardDueDate};
1926 $duration_name = $duration->name if $duration;
1927 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1930 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1931 return $self->bail_on_events($evt) if ($evt && !$nobail);
1933 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1934 return $self->bail_on_events($evt) if ($evt && !$nobail);
1936 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1937 return $self->bail_on_events($evt) if ($evt && !$nobail);
1939 if($hard_due_date_name) {
1940 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1941 return $self->bail_on_events($evt) if ($evt && !$nobail);
1947 # The item circulates with an unlimited duration
1951 $hard_due_date = undef;
1954 $self->duration_rule($duration);
1955 $self->recurring_fines_rule($recurring);
1956 $self->max_fine_rule($max_fine);
1957 $self->hard_due_date($hard_due_date);
1961 sub build_checkout_circ_object {
1964 my $circ = Fieldmapper::action::circulation->new;
1965 my $duration = $self->duration_rule;
1966 my $max = $self->max_fine_rule;
1967 my $recurring = $self->recurring_fines_rule;
1968 my $hard_due_date = $self->hard_due_date;
1969 my $copy = $self->copy;
1970 my $patron = $self->patron;
1971 my $duration_date_ceiling;
1972 my $duration_date_ceiling_force;
1976 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1977 $duration_date_ceiling = $policy->{duration_date_ceiling};
1978 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1980 my $dname = $duration->name;
1981 my $mname = $max->name;
1982 my $rname = $recurring->name;
1984 if($hard_due_date) {
1985 $hdname = $hard_due_date->name;
1988 $logger->debug("circulator: building circulation ".
1989 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1991 $circ->duration($policy->{duration});
1992 $circ->recurring_fine($policy->{recurring_fine});
1993 $circ->duration_rule($duration->name);
1994 $circ->recurring_fine_rule($recurring->name);
1995 $circ->max_fine_rule($max->name);
1996 $circ->max_fine($policy->{max_fine});
1997 $circ->fine_interval($recurring->recurrence_interval);
1998 $circ->renewal_remaining($duration->max_renewals);
1999 $circ->grace_period($policy->{grace_period});
2003 $logger->info("circulator: copy found with an unlimited circ duration");
2004 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2005 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2006 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2007 $circ->renewal_remaining(0);
2008 $circ->grace_period(0);
2011 $circ->target_copy( $copy->id );
2012 $circ->usr( $patron->id );
2013 $circ->circ_lib( $self->circ_lib );
2014 $circ->workstation($self->editor->requestor->wsid)
2015 if defined $self->editor->requestor->wsid;
2017 # renewals maintain a link to the parent circulation
2018 $circ->parent_circ($self->parent_circ);
2020 if( $self->is_renewal ) {
2021 $circ->opac_renewal('t') if $self->opac_renewal;
2022 $circ->phone_renewal('t') if $self->phone_renewal;
2023 $circ->desk_renewal('t') if $self->desk_renewal;
2024 $circ->renewal_remaining($self->renewal_remaining);
2025 $circ->circ_staff($self->editor->requestor->id);
2029 # if the user provided an overiding checkout time,
2030 # (e.g. the checkout really happened several hours ago), then
2031 # we apply that here. Does this need a perm??
2032 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2033 if $self->checkout_time;
2035 # if a patron is renewing, 'requestor' will be the patron
2036 $circ->circ_staff($self->editor->requestor->id);
2037 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2042 sub do_reservation_pickup {
2045 $self->log_me("do_reservation_pickup()");
2047 $self->reservation->pickup_time('now');
2050 $self->reservation->current_resource &&
2051 $U->is_true($self->reservation->target_resource_type->catalog_item)
2053 # We used to try to set $self->copy and $self->patron here,
2054 # but that should already be done.
2056 $self->run_checkout_scripts(1);
2058 my $duration = $self->duration_rule;
2059 my $max = $self->max_fine_rule;
2060 my $recurring = $self->recurring_fines_rule;
2062 if ($duration && $max && $recurring) {
2063 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2065 my $dname = $duration->name;
2066 my $mname = $max->name;
2067 my $rname = $recurring->name;
2069 $logger->debug("circulator: updating reservation ".
2070 "with duration=$dname, maxfine=$mname, recurring=$rname");
2072 $self->reservation->fine_amount($policy->{recurring_fine});
2073 $self->reservation->max_fine($policy->{max_fine});
2074 $self->reservation->fine_interval($recurring->recurrence_interval);
2077 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2078 $self->update_copy();
2081 $self->reservation->fine_amount(
2082 $self->reservation->target_resource_type->fine_amount
2084 $self->reservation->max_fine(
2085 $self->reservation->target_resource_type->max_fine
2087 $self->reservation->fine_interval(
2088 $self->reservation->target_resource_type->fine_interval
2092 $self->update_reservation();
2095 sub do_reservation_return {
2097 my $request = shift;
2099 $self->log_me("do_reservation_return()");
2101 if (not ref $self->reservation) {
2102 my ($reservation, $evt) =
2103 $U->fetch_booking_reservation($self->reservation);
2104 return $self->bail_on_events($evt) if $evt;
2105 $self->reservation($reservation);
2108 $self->generate_fines(1);
2109 $self->reservation->return_time('now');
2110 $self->update_reservation();
2111 $self->reshelve_copy if $self->copy;
2113 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2114 $self->copy( $self->reservation->current_resource->catalog_item );
2118 sub booking_adjusted_due_date {
2120 my $circ = $self->circ;
2121 my $copy = $self->copy;
2123 return undef unless $self->use_booking;
2127 if( $self->due_date ) {
2129 return $self->bail_on_events($self->editor->event)
2130 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2132 $circ->due_date(cleanse_ISO8601($self->due_date));
2136 return unless $copy and $circ->due_date;
2139 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2140 if (@$booking_items) {
2141 my $booking_item = $booking_items->[0];
2142 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2144 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2145 my $shorten_circ_setting = $resource_type->elbow_room ||
2146 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2149 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2150 my $bookings = $booking_ses->request(
2151 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2152 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2154 $booking_ses->disconnect;
2156 my $dt_parser = DateTime::Format::ISO8601->new;
2157 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2159 for my $bid (@$bookings) {
2161 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2163 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2164 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2166 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2167 if ($booking_start < DateTime->now);
2170 if ($U->is_true($stop_circ_setting)) {
2171 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2173 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2174 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2177 # We set the circ duration here only to affect the logic that will
2178 # later (in a DB trigger) mangle the time part of the due date to
2179 # 11:59pm. Having any circ duration that is not a whole number of
2180 # days is enough to prevent the "correction."
2181 my $new_circ_duration = $due_date->epoch - time;
2182 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2183 $circ->duration("$new_circ_duration seconds");
2185 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2189 return $self->bail_on_events($self->editor->event)
2190 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2196 sub apply_modified_due_date {
2198 my $shift_earlier = shift;
2199 my $circ = $self->circ;
2200 my $copy = $self->copy;
2202 if( $self->due_date ) {
2204 return $self->bail_on_events($self->editor->event)
2205 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2207 $circ->due_date(cleanse_ISO8601($self->due_date));
2211 # if the due_date lands on a day when the location is closed
2212 return unless $copy and $circ->due_date;
2214 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2216 # due-date overlap should be determined by the location the item
2217 # is checked out from, not the owning or circ lib of the item
2218 my $org = $self->circ_lib;
2220 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2221 " with an item due date of ".$circ->due_date );
2223 my $dateinfo = $U->storagereq(
2224 'open-ils.storage.actor.org_unit.closed_date.overlap',
2225 $org, $circ->due_date );
2228 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2229 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2231 # XXX make the behavior more dynamic
2232 # for now, we just push the due date to after the close date
2233 if ($shift_earlier) {
2234 $circ->due_date($dateinfo->{start});
2236 $circ->due_date($dateinfo->{end});
2244 sub create_due_date {
2245 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2247 # if there is a raw time component (e.g. from postgres),
2248 # turn it into an interval that interval_to_seconds can parse
2249 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2251 # for now, use the server timezone. TODO: use workstation org timezone
2252 my $due_date = DateTime->now(time_zone => 'local');
2253 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2255 # add the circ duration
2256 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2259 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2260 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2261 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2266 # return ISO8601 time with timezone
2267 return $due_date->strftime('%FT%T%z');
2272 sub make_precat_copy {
2274 my $copy = $self->copy;
2277 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2279 $copy->editor($self->editor->requestor->id);
2280 $copy->edit_date('now');
2281 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2282 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2283 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2284 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2285 $self->update_copy();
2289 $logger->info("circulator: Creating a new precataloged ".
2290 "copy in checkout with barcode " . $self->copy_barcode);
2292 $copy = Fieldmapper::asset::copy->new;
2293 $copy->circ_lib($self->circ_lib);
2294 $copy->creator($self->editor->requestor->id);
2295 $copy->editor($self->editor->requestor->id);
2296 $copy->barcode($self->copy_barcode);
2297 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2298 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2299 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2301 $copy->dummy_title($self->dummy_title || "");
2302 $copy->dummy_author($self->dummy_author || "");
2303 $copy->dummy_isbn($self->dummy_isbn || "");
2304 $copy->circ_modifier($self->circ_modifier);
2307 # See if we need to override the circ_lib for the copy with a configured circ_lib
2308 # Setting is shortname of the org unit
2309 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2310 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2312 if($precat_circ_lib) {
2313 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2316 $self->bail_on_events($self->editor->event);
2320 $copy->circ_lib($org->id);
2324 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2326 $self->push_events($self->editor->event);
2330 # this is a little bit of a hack, but we need to
2331 # get the copy into the script runner
2332 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2336 sub checkout_noncat {
2342 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2343 my $count = $self->noncat_count || 1;
2344 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2346 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2350 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2351 $self->editor->requestor->id,
2359 $self->push_events($evt);
2367 # If a copy goes into transit and is then checked in before the transit checkin
2368 # interval has expired, push an event onto the overridable events list.
2369 sub check_transit_checkin_interval {
2372 # only concerned with in-transit items
2373 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2375 # no interval, no problem
2376 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2377 return unless $interval;
2379 # capture the transit so we don't have to fetch it again later during checkin
2381 $self->editor->search_action_transit_copy(
2382 {target_copy => $self->copy->id, dest_recv_time => undef}
2386 # transit from X to X for whatever reason has no min interval
2387 return if $self->transit->source == $self->transit->dest;
2389 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2390 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2391 my $horizon = $t_start->add(seconds => $seconds);
2393 # See if we are still within the transit checkin forbidden range
2394 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2395 if $horizon > DateTime->now;
2398 # Retarget local holds at checkin
2399 sub checkin_retarget {
2401 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2402 return unless $self->is_checkin; # Renewals need not be checked
2403 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2404 return if $self->is_precat; # No holds for precats
2405 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2406 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2407 my $status = $U->copy_status($self->copy->status);
2408 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2409 # Specifically target items that are likely new (by status ID)
2410 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2411 my $location = $self->copy->location;
2412 if(!ref($location)) {
2413 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2414 $self->copy->location($location);
2416 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2418 # Fetch holds for the bib
2419 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2420 $self->editor->authtoken,
2423 capture_time => undef, # No touching captured holds
2424 frozen => 'f', # Don't bother with frozen holds
2425 pickup_lib => $self->circ_lib # Only holds actually here
2428 # Error? Skip the step.
2429 return if exists $result->{"ilsevent"};
2433 foreach my $holdlist (keys %{$result}) {
2434 push @$holds, @{$result->{$holdlist}};
2437 return if scalar(@$holds) == 0; # No holds, no retargeting
2439 # Check for parts on this copy
2440 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2441 my %parts_hash = ();
2442 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2444 # Loop over holds in request-ish order
2445 # Stage 1: Get them into request-ish order
2446 # Also grab type and target for skipping low hanging ones
2447 $result = $self->editor->json_query({
2448 "select" => { "ahr" => ["id", "hold_type", "target"] },
2449 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2450 "where" => { "id" => $holds },
2452 { "class" => "pgt", "field" => "hold_priority"},
2453 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2454 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2455 { "class" => "ahr", "field" => "request_time"}
2460 if (ref $result eq "ARRAY" and scalar @$result) {
2461 foreach (@{$result}) {
2462 # Copy level, but not this copy?
2463 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2464 and $_->{target} != $self->copy->id);
2465 # Volume level, but not this volume?
2466 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2467 if(@$parts) { # We have parts?
2469 next if ($_->{hold_type} eq 'T');
2470 # Skip part holds for parts not on this copy
2471 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2473 # No parts, no part holds
2474 next if ($_->{hold_type} eq 'P');
2476 # So much for easy stuff, attempt a retarget!
2477 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2478 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2479 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2487 $self->log_me("do_checkin()");
2489 return $self->bail_on_events(
2490 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2493 $self->check_transit_checkin_interval;
2494 $self->checkin_retarget;
2496 # the renew code and mk_env should have already found our circulation object
2497 unless( $self->circ ) {
2499 my $circs = $self->editor->search_action_circulation(
2500 { target_copy => $self->copy->id, checkin_time => undef });
2502 $self->circ($$circs[0]);
2504 # for now, just warn if there are multiple open circs on a copy
2505 $logger->warn("circulator: we have ".scalar(@$circs).
2506 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2509 my $ignore_stop_fines = undef;
2512 # if this circ is LOST and we are configured to generate overdue
2513 # fines for lost items on checkin (to fill the gap between mark
2514 # lost time and when the fines would have naturally stopped), tell
2515 # the fine generator to ignore the stop-fines value on this circ.
2516 my $stat = $U->copy_status($self->copy->status)->id;
2517 if ($stat == OILS_COPY_STATUS_LOST) {
2518 $ignore_stop_fines = $self->circ->stop_fines if
2519 $U->ou_ancestor_setting_value(
2521 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2526 # run the fine generator against this circ
2527 $self->generate_fines_start(undef, $ignore_stop_fines);
2530 if( $self->checkin_check_holds_shelf() ) {
2531 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2532 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2533 if($self->fake_hold_dest) {
2534 $self->hold->pickup_lib($self->circ_lib);
2536 $self->checkin_flesh_events;
2540 unless( $self->is_renewal ) {
2541 return $self->bail_on_events($self->editor->event)
2542 unless $self->editor->allowed('COPY_CHECKIN');
2545 $self->push_events($self->check_copy_alert());
2546 $self->push_events($self->check_checkin_copy_status());
2548 # if the circ is marked as 'claims returned', add the event to the list
2549 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2550 if ($self->circ and $self->circ->stop_fines
2551 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2553 $self->check_circ_deposit();
2555 # handle the overridable events
2556 $self->override_events unless $self->is_renewal;
2557 return if $self->bail_out;
2559 if( $self->copy and !$self->transit ) {
2561 $self->editor->search_action_transit_copy(
2562 { target_copy => $self->copy->id, dest_recv_time => undef }
2568 $self->generate_fines_finish;
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 # gather any updates to the circ after fine generation, if there was a circ
2794 $self->generate_fines_finish;
2796 return unless $self->backdate or $self->void_overdues;
2798 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2799 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2801 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2802 $self->editor, $self->circ, $self->backdate, $note);
2804 return $self->bail_on_events($evt) if $evt;
2806 # Make sure the circ is open or closed as necessary.
2807 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2808 return $self->bail_on_events($evt) if $evt;
2814 # if a deposit was payed for this item, push the event
2815 sub check_circ_deposit {
2817 return unless $self->circ;
2818 my $deposit = $self->editor->search_money_billing(
2820 xact => $self->circ->id,
2822 }, {idlist => 1})->[0];
2824 $self->push_events(OpenILS::Event->new(
2825 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2830 my $force = $self->force || shift;
2831 my $copy = $self->copy;
2833 my $stat = $U->copy_status($copy->status)->id;
2836 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2837 $stat != OILS_COPY_STATUS_CATALOGING and
2838 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2839 $stat != OILS_COPY_STATUS_RESHELVING )) {
2841 $copy->status( OILS_COPY_STATUS_RESHELVING );
2843 $self->checkin_changed(1);
2848 # Returns true if the item is at the current location
2849 # because it was transited there for a hold and the
2850 # hold has not been fulfilled
2851 sub checkin_check_holds_shelf {
2853 return 0 unless $self->copy;
2856 $U->copy_status($self->copy->status)->id ==
2857 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2859 # Attempt to clear shelf expired holds for this copy
2860 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2861 if($self->clear_expired);
2863 # find the hold that put us on the holds shelf
2864 my $holds = $self->editor->search_action_hold_request(
2866 current_copy => $self->copy->id,
2867 capture_time => { '!=' => undef },
2868 fulfillment_time => undef,
2869 cancel_time => undef,
2874 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2875 $self->reshelve_copy(1);
2879 my $hold = $$holds[0];
2881 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2882 $hold->id. "] for copy ".$self->copy->barcode);
2884 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2885 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2886 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2887 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2888 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2889 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2890 $self->fake_hold_dest(1);
2896 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2897 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2901 $logger->info("circulator: hold is not for here..");
2902 $self->remote_hold($hold);
2907 sub checkin_handle_precat {
2909 my $copy = $self->copy;
2911 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2912 $copy->status(OILS_COPY_STATUS_CATALOGING);
2913 $self->update_copy();
2914 $self->checkin_changed(1);
2915 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2920 sub checkin_build_copy_transit {
2923 my $copy = $self->copy;
2924 my $transit = Fieldmapper::action::transit_copy->new;
2926 # if we are transiting an item to the shelf shelf, it's a hold transit
2927 if (my $hold = $self->remote_hold) {
2928 $transit = Fieldmapper::action::hold_transit_copy->new;
2929 $transit->hold($hold->id);
2931 # the item is going into transit, remove any shelf-iness
2932 if ($hold->current_shelf_lib or $hold->shelf_time) {
2933 $hold->clear_current_shelf_lib;
2934 $hold->clear_shelf_time;
2935 return $self->bail_on_events($self->editor->event)
2936 unless $self->editor->update_action_hold_request($hold);
2940 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2941 $logger->info("circulator: transiting copy to $dest");
2943 $transit->source($self->circ_lib);
2944 $transit->dest($dest);
2945 $transit->target_copy($copy->id);
2946 $transit->source_send_time('now');
2947 $transit->copy_status( $U->copy_status($copy->status)->id );
2949 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2951 if ($self->remote_hold) {
2952 return $self->bail_on_events($self->editor->event)
2953 unless $self->editor->create_action_hold_transit_copy($transit);
2955 return $self->bail_on_events($self->editor->event)
2956 unless $self->editor->create_action_transit_copy($transit);
2959 # ensure the transit is returned to the caller
2960 $self->transit($transit);
2962 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2964 $self->checkin_changed(1);
2968 sub hold_capture_is_possible {
2970 my $copy = $self->copy;
2972 # we've been explicitly told not to capture any holds
2973 return 0 if $self->capture eq 'nocapture';
2975 # See if this copy can fulfill any holds
2976 my $hold = $holdcode->find_nearest_permitted_hold(
2977 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2979 return undef if ref $hold eq "HASH" and
2980 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2984 sub reservation_capture_is_possible {
2986 my $copy = $self->copy;
2988 # we've been explicitly told not to capture any holds
2989 return 0 if $self->capture eq 'nocapture';
2991 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2992 my $resv = $booking_ses->request(
2993 "open-ils.booking.reservations.could_capture",
2994 $self->editor->authtoken, $copy->barcode
2996 $booking_ses->disconnect;
2997 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2998 $self->push_events($resv);
3004 # returns true if the item was used (or may potentially be used
3005 # in subsequent calls) to capture a hold.
3006 sub attempt_checkin_hold_capture {
3008 my $copy = $self->copy;
3010 # we've been explicitly told not to capture any holds
3011 return 0 if $self->capture eq 'nocapture';
3013 # See if this copy can fulfill any holds
3014 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3015 $self->editor, $copy, $self->editor->requestor );
3018 $logger->debug("circulator: no potential permitted".
3019 "holds found for copy ".$copy->barcode);
3023 if($self->capture ne 'capture') {
3024 # see if this item is in a hold-capture-delay location
3025 my $location = $self->copy->location;
3026 if(!ref($location)) {
3027 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3028 $self->copy->location($location);
3030 if($U->is_true($location->hold_verify)) {
3031 $self->bail_on_events(
3032 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3037 $self->retarget($retarget);
3039 my $suppress_transit = 0;
3040 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3041 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3042 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3043 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3044 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3045 $suppress_transit = 1;
3046 $hold->pickup_lib($self->circ_lib);
3051 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3053 $hold->current_copy($copy->id);
3054 $hold->capture_time('now');
3055 $self->put_hold_on_shelf($hold)
3056 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3058 # prevent DB errors caused by fetching
3059 # holds from storage, and updating through cstore
3060 $hold->clear_fulfillment_time;
3061 $hold->clear_fulfillment_staff;
3062 $hold->clear_fulfillment_lib;
3063 $hold->clear_expire_time;
3064 $hold->clear_cancel_time;
3065 $hold->clear_prev_check_time unless $hold->prev_check_time;
3067 $self->bail_on_events($self->editor->event)
3068 unless $self->editor->update_action_hold_request($hold);
3070 $self->checkin_changed(1);
3072 return 0 if $self->bail_out;
3074 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3076 if ($hold->hold_type eq 'R') {
3077 $copy->status(OILS_COPY_STATUS_CATALOGING);
3078 $hold->fulfillment_time('now');
3079 $self->noop(1); # Block other transit/hold checks
3080 $self->bail_on_events($self->editor->event)
3081 unless $self->editor->update_action_hold_request($hold);
3083 # This hold was captured in the correct location
3084 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3085 $self->push_events(OpenILS::Event->new('SUCCESS'));
3087 #$self->do_hold_notify($hold->id);
3088 $self->notify_hold($hold->id);
3093 # Hold needs to be picked up elsewhere. Build a hold
3094 # transit and route the item.
3095 $self->checkin_build_hold_transit();
3096 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3097 return 0 if $self->bail_out;
3098 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3101 # make sure we save the copy status
3103 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3107 sub attempt_checkin_reservation_capture {
3109 my $copy = $self->copy;
3111 # we've been explicitly told not to capture any holds
3112 return 0 if $self->capture eq 'nocapture';
3114 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3115 my $evt = $booking_ses->request(
3116 "open-ils.booking.resources.capture_for_reservation",
3117 $self->editor->authtoken,
3119 1 # don't update copy - we probably have it locked
3121 $booking_ses->disconnect;
3123 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3125 "open-ils.booking.resources.capture_for_reservation " .
3126 "didn't return an event!"
3130 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3131 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3133 # not-transferable is an error event we'll pass on the user
3134 $logger->warn("reservation capture attempted against non-transferable item");
3135 $self->push_events($evt);
3137 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3138 # Re-retrieve copy as reservation capture may have changed
3139 # its status and whatnot.
3141 "circulator: booking capture win on copy " . $self->copy->id
3143 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3145 "circulator: changing copy " . $self->copy->id .
3146 "'s status from " . $self->copy->status . " to " .
3149 $self->copy->status($new_copy_status);
3152 $self->reservation($evt->{"payload"}->{"reservation"});
3154 if (exists $evt->{"payload"}->{"transit"}) {
3158 "org" => $evt->{"payload"}->{"transit"}->dest
3162 $self->checkin_changed(1);
3166 # other results are treated as "nothing to capture"
3170 sub do_hold_notify {
3171 my( $self, $holdid ) = @_;
3173 my $e = new_editor(xact => 1);
3174 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3176 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3177 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3179 $logger->info("circulator: running delayed hold notify process");
3181 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3182 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3184 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3185 hold_id => $holdid, requestor => $self->editor->requestor);
3187 $logger->debug("circulator: built hold notifier");
3189 if(!$notifier->event) {
3191 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3193 my $stat = $notifier->send_email_notify;
3194 if( $stat == '1' ) {
3195 $logger->info("circulator: hold notify succeeded for hold $holdid");
3199 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3202 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3206 sub retarget_holds {
3208 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3209 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3210 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3211 # no reason to wait for the return value
3215 sub checkin_build_hold_transit {
3218 my $copy = $self->copy;
3219 my $hold = $self->hold;
3220 my $trans = Fieldmapper::action::hold_transit_copy->new;
3222 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3224 $trans->hold($hold->id);
3225 $trans->source($self->circ_lib);
3226 $trans->dest($hold->pickup_lib);
3227 $trans->source_send_time("now");
3228 $trans->target_copy($copy->id);
3230 # when the copy gets to its destination, it will recover
3231 # this status - put it onto the holds shelf
3232 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3234 return $self->bail_on_events($self->editor->event)
3235 unless $self->editor->create_action_hold_transit_copy($trans);
3240 sub process_received_transit {
3242 my $copy = $self->copy;
3243 my $copyid = $self->copy->id;
3245 my $status_name = $U->copy_status($copy->status)->name;
3246 $logger->debug("circulator: attempting transit receive on ".
3247 "copy $copyid. Copy status is $status_name");
3249 my $transit = $self->transit;
3251 # Check if we are in a transit suppress range
3252 my $suppress_transit = 0;
3253 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3254 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3255 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3256 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3257 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3258 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3259 $suppress_transit = 1;
3260 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3264 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3265 # - this item is in-transit to a different location
3266 # - Or we are capturing holds as transits, so why create a new transit?
3268 my $tid = $transit->id;
3269 my $loc = $self->circ_lib;
3270 my $dest = $transit->dest;
3272 $logger->info("circulator: Fowarding transit on copy which is destined ".
3273 "for a different location. transit=$tid, copy=$copyid, current ".
3274 "location=$loc, destination location=$dest");
3276 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3278 # grab the associated hold object if available
3279 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3280 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3282 return $self->bail_on_events($evt);
3285 # The transit is received, set the receive time
3286 $transit->dest_recv_time('now');
3287 $self->bail_on_events($self->editor->event)
3288 unless $self->editor->update_action_transit_copy($transit);
3290 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3292 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3293 $copy->status( $transit->copy_status );
3294 $self->update_copy();
3295 return if $self->bail_out;
3299 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3302 # hold has arrived at destination, set shelf time
3303 $self->put_hold_on_shelf($hold);
3304 $self->bail_on_events($self->editor->event)
3305 unless $self->editor->update_action_hold_request($hold);
3306 return if $self->bail_out;
3308 $self->notify_hold($hold_transit->hold);
3311 $hold_transit = undef;
3312 $self->cancelled_hold_transit(1);
3313 $self->reshelve_copy(1);
3314 $self->fake_hold_dest(0);
3319 OpenILS::Event->new(
3322 payload => { transit => $transit, holdtransit => $hold_transit } ));
3324 return $hold_transit;
3328 # ------------------------------------------------------------------
3329 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3330 # ------------------------------------------------------------------
3331 sub put_hold_on_shelf {
3332 my($self, $hold) = @_;
3333 $hold->shelf_time('now');
3334 $hold->current_shelf_lib($self->circ_lib);
3335 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3341 sub generate_fines {
3343 my $reservation = shift;
3345 $self->generate_fines_start($reservation);
3346 $self->generate_fines_finish($reservation);
3351 sub generate_fines_start {
3353 my $reservation = shift;
3354 my $ignore_stop_fines = shift;
3355 my $dt_parser = DateTime::Format::ISO8601->new;
3357 my $obj = $reservation ? $self->reservation : $self->circ;
3359 # If we have a grace period
3360 if($obj->can('grace_period')) {
3361 # Parse out the due date
3362 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3363 # Add the grace period to the due date
3364 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3365 # Don't generate fines on circs still in grace period
3366 return undef if ($due_date > DateTime->now);
3369 if (!exists($self->{_gen_fines_req})) {
3370 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3372 'open-ils.storage.action.circulation.overdue.generate_fines',
3373 $obj->id, $ignore_stop_fines
3380 sub generate_fines_finish {
3382 my $reservation = shift;
3384 return undef unless $self->{_gen_fines_req};
3386 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3388 $self->{_gen_fines_req}->wait_complete;
3389 delete($self->{_gen_fines_req});
3391 # refresh the circ in case the fine generator set the stop_fines field
3392 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3393 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3398 sub checkin_handle_circ {
3400 my $circ = $self->circ;
3401 my $copy = $self->copy;
3405 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3407 # backdate the circ if necessary
3408 if($self->backdate) {
3409 my $evt = $self->checkin_handle_backdate;
3410 return $self->bail_on_events($evt) if $evt;
3413 if(!$circ->stop_fines) {
3414 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3415 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3416 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3417 $circ->stop_fines_time('now');
3418 $circ->stop_fines_time($self->backdate) if $self->backdate;
3421 # Set the checkin vars since we have the item
3422 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3424 # capture the true scan time for back-dated checkins
3425 $circ->checkin_scan_time('now');
3427 $circ->checkin_staff($self->editor->requestor->id);
3428 $circ->checkin_lib($self->circ_lib);
3429 $circ->checkin_workstation($self->editor->requestor->wsid);
3431 my $circ_lib = (ref $self->copy->circ_lib) ?
3432 $self->copy->circ_lib->id : $self->copy->circ_lib;
3433 my $stat = $U->copy_status($self->copy->status)->id;
3435 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3436 # we will now handle lost fines, but the copy will retain its 'lost'
3437 # status if it needs to transit home unless lost_immediately_available
3440 # if we decide to also delay fine handling until the item arrives home,
3441 # we will need to call lost fine handling code both when checking items
3442 # in and also when receiving transits
3443 $self->checkin_handle_lost($circ_lib);
3444 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3445 # same process as above.
3446 $self->checkin_handle_long_overdue($circ_lib);
3447 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3448 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3450 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3455 # see if there are any fines owed on this circ. if not, close it
3456 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3457 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3459 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3461 return $self->bail_on_events($self->editor->event)
3462 unless $self->editor->update_action_circulation($circ);
3467 # ------------------------------------------------------------------
3468 # See if we need to void billings, etc. for lost checkin
3469 # ------------------------------------------------------------------
3470 sub checkin_handle_lost {
3472 my $circ_lib = shift;
3474 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3475 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3477 return $self->checkin_handle_lost_or_longoverdue(
3478 circ_lib => $circ_lib,
3479 max_return => $max_return,
3480 ous_dont_change_on_zero => 'circ.checkin.lost_zero_balance.do_not_change',
3481 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3482 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3483 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3484 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3485 ous_use_last_activity => undef, # not supported for LOST checkin
3486 void_cost_btype => 3,
3491 # ------------------------------------------------------------------
3492 # See if we need to void billings, etc. for long-overdue checkin
3493 # note: not using constants below since they serve little purpose
3494 # for single-use strings that are descriptive in their own right
3495 # and mostly just complicate debugging.
3496 # ------------------------------------------------------------------
3497 sub checkin_handle_long_overdue {
3499 my $circ_lib = shift;
3501 $logger->info("circulator: processing long-overdue checkin...");
3503 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3504 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3506 return $self->checkin_handle_lost_or_longoverdue(
3507 circ_lib => $circ_lib,
3508 max_return => $max_return,
3509 is_longoverdue => 1,
3510 ous_dont_change_on_zero => 'circ.checkin.lost_zero_balance.do_not_change',
3511 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3512 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3513 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3514 ous_immediately_available => 'circ.longoverdue_immediately_available',
3515 ous_use_last_activity =>
3516 'circ.longoverdue.use_last_activity_date_on_return',
3517 void_cost_btype => 10,
3518 void_fee_btype => 11
3522 # last billing activity is last payment time, last billing time, or the
3523 # circ due date. If the relevant "use last activity" org unit setting is
3524 # false/unset, then last billing activity is always the due date.
3525 sub get_circ_last_billing_activity {
3527 my $circ_lib = shift;
3528 my $setting = shift;
3529 my $date = $self->circ->due_date;
3531 return $date unless $setting and
3532 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3534 my $xact = $self->editor->retrieve_money_billable_transaction([
3536 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3539 if ($xact->summary) {
3540 $date = $xact->summary->last_payment_ts ||
3541 $xact->summary->last_billing_ts ||
3542 $self->circ->due_date;
3549 sub checkin_handle_lost_or_longoverdue {
3550 my ($self, %args) = @_;
3552 my $circ = $self->circ;
3553 my $max_return = $args{max_return};
3554 my $circ_lib = $args{circ_lib};
3559 $self->get_circ_last_billing_activity(
3560 $circ_lib, $args{ous_use_last_activity});
3563 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3564 $tm[5] -= 1 if $tm[5] > 0;
3565 my $due = timelocal(int($tm[1]), int($tm[2]),
3566 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3569 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3571 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3572 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3573 "DUE: $due LAST: $last_chance");
3575 $max_return = 0 if $today < $last_chance;
3581 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3582 "return interval. skipping fine/fee voiding, etc.");
3584 } else { # within max-return interval or no interval defined
3586 $logger->info("circulator: check-in of lost/lo item is within the ".
3587 "max return interval (or no interval is defined). Proceeding ".
3588 "with fine/fee voiding, etc.");
3590 my $dont_change = $U->ou_ancestor_setting_value(
3591 $circ_lib, $args{ous_dont_change_on_zero}, $self->editor) || 0;
3594 my ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3595 $dont_change = 0 if( $obt and $obt->balance_owed != 0 );
3598 $logger->info("circulator: check-in of lost/lo item having a balance ".
3599 "of zero, skipping fine/fee voiding and reinstatement.") if ($dont_change);
3601 my $void_cost = $U->ou_ancestor_setting_value(
3602 $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3603 my $void_proc_fee = $U->ou_ancestor_setting_value(
3604 $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3605 my $restore_od = $U->ou_ancestor_setting_value(
3606 $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3608 $self->checkin_handle_lost_or_lo_now_found(
3609 $args{void_cost_btype}, $args{is_longoverdue}) if ($void_cost and !$dont_change);
3610 $self->checkin_handle_lost_or_lo_now_found(
3611 $args{void_fee_btype}, $args{is_longoverdue}) if ($void_proc_fee and !$dont_change);
3612 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3613 if ! $dont_change && $restore_od && ! $self->void_overdues;
3616 if ($circ_lib != $self->circ_lib) {
3617 # if the item is not home, check to see if we want to retain the
3618 # lost/longoverdue status at this point in the process
3620 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3621 $args{ous_immediately_available}, $self->editor) || 0;
3623 if ($immediately_available) {
3624 # item status does not need to be retained, so give it a
3625 # reshelving status as if it were a normal checkin
3626 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3629 $logger->info("circulator: leaving lost/longoverdue copy".
3630 " status in place on checkin");
3633 # lost/longoverdue item is home and processed, treat like a normal
3634 # checkin from this point on
3635 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3641 sub checkin_handle_backdate {
3644 # ------------------------------------------------------------------
3645 # clean up the backdate for date comparison
3646 # XXX We are currently taking the due-time from the original due-date,
3647 # not the input. Do we need to do this? This certainly interferes with
3648 # backdating of hourly checkouts, but that is likely a very rare case.
3649 # ------------------------------------------------------------------
3650 my $bd = cleanse_ISO8601($self->backdate);
3651 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3652 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3653 $new_date->set_hour($original_date->hour());
3654 $new_date->set_minute($original_date->minute());
3655 if ($new_date >= DateTime->now) {
3656 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3659 $bd = cleanse_ISO8601($new_date->datetime());
3662 $self->backdate($bd);
3667 sub check_checkin_copy_status {
3669 my $copy = $self->copy;
3671 my $status = $U->copy_status($copy->status)->id;
3674 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3675 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3676 $status == OILS_COPY_STATUS_IN_PROCESS ||
3677 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3678 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3679 $status == OILS_COPY_STATUS_CATALOGING ||
3680 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3681 $status == OILS_COPY_STATUS_RESHELVING );
3683 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3684 if( $status == OILS_COPY_STATUS_LOST );
3686 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3687 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3689 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3690 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3692 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3693 if( $status == OILS_COPY_STATUS_MISSING );
3695 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3700 # --------------------------------------------------------------------------
3701 # On checkin, we need to return as many relevant objects as we can
3702 # --------------------------------------------------------------------------
3703 sub checkin_flesh_events {
3706 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3707 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3708 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3711 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3714 if($self->hold and !$self->hold->cancel_time) {
3715 $hold = $self->hold;
3716 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3720 # update our copy of the circ object and
3721 # flesh the billing summary data
3723 $self->editor->retrieve_action_circulation([
3727 circ => ['billable_transaction'],
3736 # flesh some patron fields before returning
3738 $self->editor->retrieve_actor_user([
3743 au => ['card', 'billing_address', 'mailing_address']
3750 for my $evt (@{$self->events}) {
3753 $payload->{copy} = $U->unflesh_copy($self->copy);
3754 $payload->{volume} = $self->volume;
3755 $payload->{record} = $record,
3756 $payload->{circ} = $self->circ;
3757 $payload->{transit} = $self->transit;
3758 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3759 $payload->{hold} = $hold;
3760 $payload->{patron} = $self->patron;
3761 $payload->{reservation} = $self->reservation
3762 unless (not $self->reservation or $self->reservation->cancel_time);
3764 $evt->{payload} = $payload;
3769 my( $self, $msg ) = @_;
3770 my $bc = ($self->copy) ? $self->copy->barcode :
3773 my $usr = ($self->patron) ? $self->patron->id : "";
3774 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3775 ", recipient=$usr, copy=$bc");
3781 $self->log_me("do_renew()");
3783 # Make sure there is an open circ to renew that is not
3784 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3785 my $usrid = $self->patron->id if $self->patron;
3786 my $circ = $self->editor->search_action_circulation({
3787 target_copy => $self->copy->id,
3788 xact_finish => undef,
3789 checkin_time => undef,
3790 ($usrid ? (usr => $usrid) : ()),
3792 {stop_fines => undef},
3793 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3797 return $self->bail_on_events($self->editor->event) unless $circ;
3799 # A user is not allowed to renew another user's items without permission
3800 unless( $circ->usr eq $self->editor->requestor->id ) {
3801 return $self->bail_on_events($self->editor->events)
3802 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3805 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3806 if $circ->renewal_remaining < 1;
3808 # -----------------------------------------------------------------
3810 $self->parent_circ($circ->id);
3811 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3814 # Opac renewal - re-use circ library from original circ (unless told not to)
3815 if($self->opac_renewal) {
3816 unless(defined($opac_renewal_use_circ_lib)) {
3817 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3818 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3819 $opac_renewal_use_circ_lib = 1;
3822 $opac_renewal_use_circ_lib = 0;
3825 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3828 # Desk renewal - re-use circ library from original circ (unless told not to)
3829 if($self->desk_renewal) {
3830 unless(defined($desk_renewal_use_circ_lib)) {
3831 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3832 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3833 $desk_renewal_use_circ_lib = 1;
3836 $desk_renewal_use_circ_lib = 0;
3839 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3842 # Run the fine generator against the old circ
3843 $self->generate_fines_start;
3845 $self->run_renew_permit;
3848 $self->do_checkin();
3849 return if $self->bail_out;
3851 unless( $self->permit_override ) {
3853 return if $self->bail_out;
3854 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3855 $self->remove_event('ITEM_NOT_CATALOGED');
3858 $self->override_events;
3859 return if $self->bail_out;
3862 $self->do_checkout();
3867 my( $self, $evt ) = @_;
3868 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3869 $logger->debug("circulator: removing event from list: $evt");
3870 my @events = @{$self->events};
3871 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3876 my( $self, $evt ) = @_;
3877 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3878 return grep { $_->{textcode} eq $evt } @{$self->events};
3883 sub run_renew_permit {
3886 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3887 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3888 $self->editor, $self->copy, $self->editor->requestor, 1
3890 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3893 if(!$self->legacy_script_support) {
3894 my $results = $self->run_indb_circ_test;
3895 $self->push_events($self->matrix_test_result_events)
3896 unless $self->circ_test_success;
3899 my $runner = $self->script_runner;
3901 $runner->load($self->circ_permit_renew);
3902 my $result = $runner->run or
3903 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3904 if ($result->{"events"}) {
3906 map { new OpenILS::Event($_) } @{$result->{"events"}}
3909 "circulator: circ_permit_renew for user " .
3910 $self->patron->id . " returned " .
3911 scalar(@{$result->{"events"}}) . " event(s)"
3915 $self->mk_script_runner;
3918 $logger->debug("circulator: re-creating script runner to be safe");
3922 # XXX: The primary mechanism for storing circ history is now handled
3923 # by tracking real circulation objects instead of bibs in a bucket.
3924 # However, this code is disabled by default and could be useful
3925 # some day, so may as well leave it for now.
3926 sub append_reading_list {
3930 $self->is_checkout and
3936 # verify history is globally enabled and uses the bucket mechanism
3937 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3938 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3940 return undef unless $htype and $htype eq 'bucket';
3942 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3944 # verify the patron wants to retain the hisory
3945 my $setting = $e->search_actor_user_setting(
3946 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3948 unless($setting and $setting->value) {
3953 my $bkt = $e->search_container_copy_bucket(
3954 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3959 # find the next item position
3960 my $last_item = $e->search_container_copy_bucket_item(
3961 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3962 $pos = $last_item->pos + 1 if $last_item;
3965 # create the history bucket if necessary
3966 $bkt = Fieldmapper::container::copy_bucket->new;
3967 $bkt->owner($self->patron->id);
3969 $bkt->btype('circ_history');
3971 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3974 my $item = Fieldmapper::container::copy_bucket_item->new;
3976 $item->bucket($bkt->id);
3977 $item->target_copy($self->copy->id);
3980 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3987 sub make_trigger_events {
3989 return unless $self->circ;
3990 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3991 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3992 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3997 sub checkin_handle_lost_or_lo_now_found {
3998 my ($self, $bill_type, $is_longoverdue) = @_;
4000 # ------------------------------------------------------------------
4001 # remove charge from patron's account if lost item is returned
4002 # ------------------------------------------------------------------
4004 my $bills = $self->editor->search_money_billing(
4006 xact => $self->circ->id,
4011 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4013 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
4014 for my $bill (@$bills) {
4015 if( !$U->is_true($bill->voided) ) {
4016 $logger->info("$tag item returned - voiding bill ".$bill->id);
4018 $bill->void_time('now');
4019 $bill->voider($self->editor->requestor->id);
4020 my $note = ($bill->note) ? $bill->note . "\n" : '';
4021 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
4023 $self->bail_on_events($self->editor->event)
4024 unless $self->editor->update_money_billing($bill);
4029 sub checkin_handle_lost_or_lo_now_found_restore_od {
4031 my $circ_lib = shift;
4032 my $is_longoverdue = shift;
4033 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4035 # ------------------------------------------------------------------
4036 # restore those overdue charges voided when item was set to lost
4037 # ------------------------------------------------------------------
4039 my $ods = $self->editor->search_money_billing(
4041 xact => $self->circ->id,
4046 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4047 for my $bill (@$ods) {
4048 if( $U->is_true($bill->voided) ) {
4049 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4051 $bill->clear_void_time;
4052 $bill->voider($self->editor->requestor->id);
4053 my $note = ($bill->note) ? $bill->note . "\n" : '';
4054 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4056 $self->bail_on_events($self->editor->event)
4057 unless $self->editor->update_money_billing($bill);