1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
11 my $U = "OpenILS::Application::AppUtils";
15 my $legacy_script_support = 0;
17 my $opac_renewal_use_circ_lib;
18 my $desk_renewal_use_circ_lib;
20 sub determine_booking_status {
21 unless (defined $booking_status) {
22 my $ses = create OpenSRF::AppSession("router");
23 $booking_status = grep {$_ eq "open-ils.booking"} @{
24 $ses->request("opensrf.router.info.class.list")->gather(1)
27 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
30 return $booking_status;
36 flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
42 my $conf = OpenSRF::Utils::SettingsClient->new;
43 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
45 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
46 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
48 my $lb = $conf->config_value( @pfx2, 'script_path' );
49 $lb = [ $lb ] unless ref($lb);
52 return unless $legacy_script_support;
54 my @pfx = ( @pfx2, "scripts" );
55 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
56 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
57 my $d = $conf->config_value( @pfx, 'circ_duration' );
58 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
59 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
60 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
62 $logger->error( "Missing circ script(s)" )
63 unless( $p and $c and $d and $f and $m and $pr );
65 $scripts{circ_permit_patron} = $p;
66 $scripts{circ_permit_copy} = $c;
67 $scripts{circ_duration} = $d;
68 $scripts{circ_recurring_fines} = $f;
69 $scripts{circ_max_fines} = $m;
70 $scripts{circ_permit_renew} = $pr;
73 "circulator: Loaded rules scripts for circ: " .
74 "circ permit patron = $p, ".
75 "circ permit copy = $c, ".
76 "circ duration = $d, ".
77 "circ recurring fines = $f, " .
78 "circ max fines = $m, ".
79 "circ renew permit = $pr. ".
81 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
85 __PACKAGE__->register_method(
86 method => "run_method",
87 api_name => "open-ils.circ.checkout.permit",
89 Determines if the given checkout can occur
90 @param authtoken The login session key
91 @param params A trailing hash of named params including
92 barcode : The copy barcode,
93 patron : The patron the checkout is occurring for,
94 renew : true or false - whether or not this is a renewal
95 @return The event that occurred during the permit check.
99 __PACKAGE__->register_method (
100 method => 'run_method',
101 api_name => 'open-ils.circ.checkout.permit.override',
102 signature => q/@see open-ils.circ.checkout.permit/,
106 __PACKAGE__->register_method(
107 method => "run_method",
108 api_name => "open-ils.circ.checkout",
111 @param authtoken The login session key
112 @param params A named hash of params including:
114 barcode If no copy is provided, the copy is retrieved via barcode
115 copyid If no copy or barcode is provide, the copy id will be use
116 patron The patron's id
117 noncat True if this is a circulation for a non-cataloted item
118 noncat_type The non-cataloged type id
119 noncat_circ_lib The location for the noncat circ.
120 precat The item has yet to be cataloged
121 dummy_title The temporary title of the pre-cataloded item
122 dummy_author The temporary authr of the pre-cataloded item
123 Default is the home org of the staff member
124 @return The SUCCESS event on success, any other event depending on the error
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.checkin",
132 Generic super-method for handling all copies
133 @param authtoken The login session key
134 @param params Hash of named parameters including:
135 barcode - The copy barcode
136 force - If true, copies in bad statuses will be checked in and give good statuses
137 noop - don't capture holds or put items into transit
138 void_overdues - void all overdues for the circulation (aka amnesty)
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkin.override",
146 signature => q/@see open-ils.circ.checkin/
149 __PACKAGE__->register_method(
150 method => "run_method",
151 api_name => "open-ils.circ.renew.override",
152 signature => q/@see open-ils.circ.renew/,
156 __PACKAGE__->register_method(
157 method => "run_method",
158 api_name => "open-ils.circ.renew",
159 notes => <<" NOTES");
160 PARAMS( authtoken, circ => circ_id );
161 open-ils.circ.renew(login_session, circ_object);
162 Renews the provided circulation. login_session is the requestor of the
163 renewal and if the logged in user is not the same as circ->usr, then
164 the logged in user must have RENEW_CIRC permissions.
167 __PACKAGE__->register_method(
168 method => "run_method",
169 api_name => "open-ils.circ.checkout.full"
171 __PACKAGE__->register_method(
172 method => "run_method",
173 api_name => "open-ils.circ.checkout.full.override"
175 __PACKAGE__->register_method(
176 method => "run_method",
177 api_name => "open-ils.circ.reservation.pickup"
179 __PACKAGE__->register_method(
180 method => "run_method",
181 api_name => "open-ils.circ.reservation.return"
183 __PACKAGE__->register_method(
184 method => "run_method",
185 api_name => "open-ils.circ.reservation.return.override"
187 __PACKAGE__->register_method(
188 method => "run_method",
189 api_name => "open-ils.circ.checkout.inspect",
190 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
195 my( $self, $conn, $auth, $args ) = @_;
196 translate_legacy_args($args);
197 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
198 my $api = $self->api_name;
201 OpenILS::Application::Circ::Circulator->new($auth, %$args);
203 return circ_events($circulator) if $circulator->bail_out;
205 $circulator->use_booking(determine_booking_status());
207 # --------------------------------------------------------------------------
208 # First, check for a booking transit, as the barcode may not be a copy
209 # barcode, but a resource barcode, and nothing else in here will work
210 # --------------------------------------------------------------------------
212 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
213 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
214 if (@$resources) { # yes!
216 my $res_id_list = [ map { $_->id } @$resources ];
217 my $transit = $circulator->editor->search_action_reservation_transit_copy(
219 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
220 { order_by => { artc => 'source_send_time' }, limit => 1 }
222 )->[0]; # Any transit for this barcode?
224 if ($transit) { # yes! unwrap it.
226 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
227 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
229 my $success_event = new OpenILS::Event(
230 "SUCCESS", "payload" => {"reservation" => $reservation}
232 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
233 if (my $copy = $circulator->editor->search_asset_copy([
234 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
235 ])->[0]) { # got a copy
236 $copy->status( $transit->copy_status );
237 $copy->editor($circulator->editor->requestor->id);
238 $copy->edit_date('now');
239 $circulator->editor->update_asset_copy($copy);
240 $success_event->{"payload"}->{"record"} =
241 $U->record_to_mvr($copy->call_number->record);
242 $success_event->{"payload"}->{"volume"} = $copy->call_number;
243 $copy->call_number($copy->call_number->id);
244 $success_event->{"payload"}->{"copy"} = $copy;
248 $transit->dest_recv_time('now');
249 $circulator->editor->update_action_reservation_transit_copy( $transit );
251 $circulator->editor->commit;
252 # Formerly this branch just stopped here. Argh!
253 $conn->respond_complete($success_event);
261 # --------------------------------------------------------------------------
262 # Go ahead and load the script runner to make sure we have all
263 # of the objects we need
264 # --------------------------------------------------------------------------
266 if ($circulator->use_booking) {
267 $circulator->is_res_checkin($circulator->is_checkin(1))
268 if $api =~ /reservation.return/ or (
269 $api =~ /checkin/ and $circulator->seems_like_reservation()
272 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
275 $circulator->is_renewal(1) if $api =~ /renew/;
276 $circulator->is_checkin(1) if $api =~ /checkin/;
278 $circulator->mk_env();
279 $circulator->noop(1) if $circulator->claims_never_checked_out;
281 if($legacy_script_support and not $circulator->is_checkin) {
282 $circulator->mk_script_runner();
283 $circulator->legacy_script_support(1);
284 $circulator->circ_permit_patron($scripts{circ_permit_patron});
285 $circulator->circ_permit_copy($scripts{circ_permit_copy});
286 $circulator->circ_duration($scripts{circ_duration});
287 $circulator->circ_permit_renew($scripts{circ_permit_renew});
289 return circ_events($circulator) if $circulator->bail_out;
292 $circulator->override(1) if $api =~ /override/o;
294 if( $api =~ /checkout\.permit/ ) {
295 $circulator->do_permit();
297 } elsif( $api =~ /checkout.full/ ) {
299 # requesting a precat checkout implies that any required
300 # overrides have been performed. Go ahead and re-override.
301 $circulator->skip_permit_key(1);
302 $circulator->override(1) if $circulator->request_precat;
303 $circulator->do_permit();
304 $circulator->is_checkout(1);
305 unless( $circulator->bail_out ) {
306 $circulator->events([]);
307 $circulator->do_checkout();
310 } elsif( $circulator->is_res_checkout ) {
311 $circulator->do_reservation_pickup();
313 } elsif( $api =~ /inspect/ ) {
314 my $data = $circulator->do_inspect();
315 $circulator->editor->rollback;
318 } elsif( $api =~ /checkout/ ) {
319 $circulator->is_checkout(1);
320 $circulator->do_checkout();
322 } elsif( $circulator->is_res_checkin ) {
323 $circulator->do_reservation_return();
324 $circulator->do_checkin() if ($circulator->copy());
325 } elsif( $api =~ /checkin/ ) {
326 $circulator->do_checkin();
328 } elsif( $api =~ /renew/ ) {
329 $circulator->is_renewal(1);
330 $circulator->do_renew();
333 if( $circulator->bail_out ) {
336 # make sure no success event accidentally slip in
338 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
341 my @e = @{$circulator->events};
342 push( @ee, $_->{textcode} ) for @e;
343 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
345 $circulator->editor->rollback;
349 $circulator->editor->commit;
352 $conn->respond_complete(circ_events($circulator));
354 $circulator->script_runner->cleanup if $circulator->script_runner;
356 return undef if $circulator->bail_out;
358 $circulator->do_hold_notify($circulator->notify_hold)
359 if $circulator->notify_hold;
360 $circulator->retarget_holds if $circulator->retarget;
361 $circulator->append_reading_list;
362 $circulator->make_trigger_events;
369 my @e = @{$circ->events};
370 # if we have multiple events, SUCCESS should not be one of them;
371 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
372 return (@e == 1) ? $e[0] : \@e;
376 sub translate_legacy_args {
379 if( $$args{barcode} ) {
380 $$args{copy_barcode} = $$args{barcode};
381 delete $$args{barcode};
384 if( $$args{copyid} ) {
385 $$args{copy_id} = $$args{copyid};
386 delete $$args{copyid};
389 if( $$args{patronid} ) {
390 $$args{patron_id} = $$args{patronid};
391 delete $$args{patronid};
394 if( $$args{patron} and !ref($$args{patron}) ) {
395 $$args{patron_id} = $$args{patron};
396 delete $$args{patron};
400 if( $$args{noncat} ) {
401 $$args{is_noncat} = $$args{noncat};
402 delete $$args{noncat};
405 if( $$args{precat} ) {
406 $$args{is_precat} = $$args{request_precat} = $$args{precat};
407 delete $$args{precat};
413 # --------------------------------------------------------------------------
414 # This package actually manages all of the circulation logic
415 # --------------------------------------------------------------------------
416 package OpenILS::Application::Circ::Circulator;
417 use strict; use warnings;
418 use vars q/$AUTOLOAD/;
420 use OpenILS::Utils::Fieldmapper;
421 use OpenSRF::Utils::Cache;
422 use Digest::MD5 qw(md5_hex);
423 use DateTime::Format::ISO8601;
424 use OpenILS::Utils::PermitHold;
425 use OpenSRF::Utils qw/:datetime/;
426 use OpenSRF::Utils::SettingsClient;
427 use OpenILS::Application::Circ::Holds;
428 use OpenILS::Application::Circ::Transit;
429 use OpenSRF::Utils::Logger qw(:logger);
430 use OpenILS::Utils::CStoreEditor qw/:funcs/;
431 use OpenILS::Application::Circ::ScriptBuilder;
432 use OpenILS::Const qw/:const/;
433 use OpenILS::Utils::Penalty;
434 use OpenILS::Application::Circ::CircCommon;
437 my $CC = "OpenILS::Application::Circ::CircCommon";
438 my $holdcode = "OpenILS::Application::Circ::Holds";
439 my $transcode = "OpenILS::Application::Circ::Transit";
445 # --------------------------------------------------------------------------
446 # Add a pile of automagic getter/setter methods
447 # --------------------------------------------------------------------------
448 my @AUTOLOAD_FIELDS = qw/
495 recurring_fines_level
508 cancelled_hold_transit
515 circ_matrix_matchpoint
517 legacy_script_support
527 claims_never_checked_out
540 dont_change_lost_zero
546 my $type = ref($self) or die "$self is not an object";
548 my $name = $AUTOLOAD;
551 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
552 $logger->error("circulator: $type: invalid autoload field: $name");
553 die "$type: invalid autoload field: $name\n"
558 *{"${type}::${name}"} = sub {
561 $s->{$name} = $v if defined $v;
565 return $self->$name($data);
570 my( $class, $auth, %args ) = @_;
571 $class = ref($class) || $class;
572 my $self = bless( {}, $class );
575 $self->editor(new_editor(xact => 1, authtoken => $auth));
577 unless( $self->editor->checkauth ) {
578 $self->bail_on_events($self->editor->event);
582 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
584 $self->$_($args{$_}) for keys %args;
587 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
589 # if this is a renewal, default to desk_renewal
590 $self->desk_renewal(1) unless
591 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
593 $self->capture('') unless $self->capture;
595 unless(%user_groups) {
596 my $gps = $self->editor->retrieve_all_permission_grp_tree;
597 %user_groups = map { $_->id => $_ } @$gps;
604 # --------------------------------------------------------------------------
605 # True if we should discontinue processing
606 # --------------------------------------------------------------------------
608 my( $self, $bool ) = @_;
609 if( defined $bool ) {
610 $logger->info("circulator: BAILING OUT") if $bool;
611 $self->{bail_out} = $bool;
613 return $self->{bail_out};
618 my( $self, @evts ) = @_;
621 $e->{payload} = $self->copy if
622 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
624 $logger->info("circulator: pushing event ".$e->{textcode});
625 push( @{$self->events}, $e ) unless
626 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
632 return '' if $self->skip_permit_key;
633 my $key = md5_hex( time() . rand() . "$$" );
634 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
635 return $self->permit_key($key);
638 sub check_permit_key {
640 return 1 if $self->skip_permit_key;
641 my $key = $self->permit_key;
642 return 0 unless $key;
643 my $k = "oils_permit_key_$key";
644 my $one = $self->cache_handle->get_cache($k);
645 $self->cache_handle->delete_cache($k);
646 return ($one) ? 1 : 0;
649 sub seems_like_reservation {
652 # Some words about the following method:
653 # 1) It requires the VIEW_USER permission, but that's not an
654 # issue, right, since all staff should have that?
655 # 2) It returns only one reservation at a time, even if an item can be
656 # and is currently overbooked. Hmmm....
657 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
658 my $result = $booking_ses->request(
659 "open-ils.booking.reservations.by_returnable_resource_barcode",
660 $self->editor->authtoken,
663 $booking_ses->disconnect;
665 return $self->bail_on_events($result) if defined $U->event_code($result);
668 $self->reservation(shift @$result);
676 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
677 sub save_trimmed_copy {
678 my ($self, $copy) = @_;
681 $self->volume($copy->call_number);
682 $self->title($self->volume->record);
683 $self->copy->call_number($self->volume->id);
684 $self->volume->record($self->title->id);
685 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
686 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
687 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
688 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
694 my $e = $self->editor;
696 # --------------------------------------------------------------------------
697 # Grab the fleshed copy
698 # --------------------------------------------------------------------------
699 unless($self->is_noncat) {
702 $copy = $e->retrieve_asset_copy(
703 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
705 } elsif( $self->copy_barcode ) {
707 $copy = $e->search_asset_copy(
708 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
709 } elsif( $self->reservation ) {
710 my $res = $e->json_query(
712 "select" => {"acp" => ["id"]},
717 "field" => "barcode",
721 "field" => "current_resource"
729 "id" => (ref $self->reservation) ?
730 $self->reservation->id : $self->reservation
735 if (ref $res eq "ARRAY" and scalar @$res) {
736 $logger->info("circulator: mapped reservation " .
737 $self->reservation . " to copy " . $res->[0]->{"id"});
738 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
743 $self->save_trimmed_copy($copy);
745 # We can't renew if there is no copy
746 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
747 if $self->is_renewal;
752 # --------------------------------------------------------------------------
754 # --------------------------------------------------------------------------
758 flesh_fields => {au => [ qw/ card / ]}
761 if( $self->patron_id ) {
762 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
763 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
765 } elsif( $self->patron_barcode ) {
767 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
768 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
769 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
771 $patron = $e->retrieve_actor_user($card->usr)
772 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
774 # Use the card we looked up, not the patron's primary, for card active checks
775 $patron->card($card);
778 if( my $copy = $self->copy ) {
781 $flesh->{flesh_fields}->{circ} = ['usr'];
783 my $circ = $e->search_action_circulation([
784 {target_copy => $copy->id, checkin_time => undef}, $flesh
788 $patron = $circ->usr;
789 $circ->usr($patron->id); # de-flesh for consistency
795 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
796 unless $self->patron($patron) or $self->is_checkin;
798 unless($self->is_checkin) {
800 # Check for inactivity and patron reg. expiration
802 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
803 unless $U->is_true($patron->active);
805 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
806 unless $U->is_true($patron->card->active);
808 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
809 cleanse_ISO8601($patron->expire_date));
811 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
812 if( CORE::time > $expire->epoch ) ;
816 # --------------------------------------------------------------------------
817 # This builds the script runner environment and fetches most of the
819 # --------------------------------------------------------------------------
820 sub mk_script_runner {
826 qw/copy copy_barcode copy_id patron
827 patron_id patron_barcode volume title editor/;
829 # Translate our objects into the ScriptBuilder args hash
830 $$args{$_} = $self->$_() for @fields;
832 $args->{ignore_user_status} = 1 if $self->is_checkin;
833 $$args{fetch_patron_by_circ_copy} = 1;
834 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
836 if( my $pco = $self->pending_checkouts ) {
837 $logger->info("circulator: we were given a pending checkouts number of $pco");
838 $$args{patronItemsOut} = $pco;
841 # This fetches most of the objects we need
842 $self->script_runner(
843 OpenILS::Application::Circ::ScriptBuilder->build($args));
845 # Now we translate the ScriptBuilder objects back into self
846 $self->$_($$args{$_}) for @fields;
848 my @evts = @{$args->{_events}} if $args->{_events};
850 $logger->debug("circulator: script builder returned events: @evts") if @evts;
854 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
855 if(!$self->is_noncat and
857 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
861 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
862 return $self->bail_on_events(@e);
867 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
868 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
869 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
870 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
874 # We can't renew if there is no copy
875 return $self->bail_on_events(@evts) if
876 $self->is_renewal and !$self->copy;
878 # Set some circ-specific flags in the script environment
879 my $evt = "environment";
880 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
882 if( $self->is_noncat ) {
883 $self->script_runner->insert("$evt.isNonCat", 1);
884 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
887 if( $self->is_precat ) {
888 $self->script_runner->insert("environment.isPrecat", 1, 1);
891 $self->script_runner->add_path( $_ ) for @$script_libs;
896 # --------------------------------------------------------------------------
897 # Does the circ permit work
898 # --------------------------------------------------------------------------
902 $self->log_me("do_permit()");
904 unless( $self->editor->requestor->id == $self->patron->id ) {
905 return $self->bail_on_events($self->editor->event)
906 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
909 $self->check_captured_holds();
910 $self->do_copy_checks();
911 return if $self->bail_out;
912 $self->run_patron_permit_scripts();
913 $self->run_copy_permit_scripts()
914 unless $self->is_precat or $self->is_noncat;
915 $self->check_item_deposit_events();
916 $self->override_events();
917 return if $self->bail_out;
919 if($self->is_precat and not $self->request_precat) {
922 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
923 return $self->bail_out(1) unless $self->is_renewal;
927 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
930 sub check_item_deposit_events {
932 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
933 if $self->is_deposit and not $self->is_deposit_exempt;
934 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
935 if $self->is_rental and not $self->is_rental_exempt;
938 # returns true if the user is not required to pay deposits
939 sub is_deposit_exempt {
941 my $pid = (ref $self->patron->profile) ?
942 $self->patron->profile->id : $self->patron->profile;
943 my $groups = $U->ou_ancestor_setting_value(
944 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
945 for my $grp (@$groups) {
946 return 1 if $self->is_group_descendant($grp, $pid);
951 # returns true if the user is not required to pay rental fees
952 sub is_rental_exempt {
954 my $pid = (ref $self->patron->profile) ?
955 $self->patron->profile->id : $self->patron->profile;
956 my $groups = $U->ou_ancestor_setting_value(
957 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
958 for my $grp (@$groups) {
959 return 1 if $self->is_group_descendant($grp, $pid);
964 sub is_group_descendant {
965 my($self, $p_id, $c_id) = @_;
966 return 0 unless defined $p_id and defined $c_id;
967 return 1 if $c_id == $p_id;
968 while(my $grp = $user_groups{$c_id}) {
969 $c_id = $grp->parent;
970 return 0 unless defined $c_id;
971 return 1 if $c_id == $p_id;
976 sub check_captured_holds {
978 my $copy = $self->copy;
979 my $patron = $self->patron;
981 return undef unless $copy;
983 my $s = $U->copy_status($copy->status)->id;
984 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
985 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
987 # Item is on the holds shelf, make sure it's going to the right person
988 my $hold = $self->editor->search_action_hold_request(
991 current_copy => $copy->id ,
992 capture_time => { '!=' => undef },
993 cancel_time => undef,
994 fulfillment_time => undef
1000 if ($hold and $hold->usr == $patron->id) {
1001 $self->checkout_is_for_hold(1);
1005 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1007 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1011 sub do_copy_checks {
1013 my $copy = $self->copy;
1014 return unless $copy;
1016 my $stat = $U->copy_status($copy->status)->id;
1018 # We cannot check out a copy if it is in-transit
1019 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1020 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1023 $self->handle_claims_returned();
1024 return if $self->bail_out;
1026 # no claims returned circ was found, check if there is any open circ
1027 unless( $self->is_renewal ) {
1029 my $circs = $self->editor->search_action_circulation(
1030 { target_copy => $copy->id, checkin_time => undef }
1033 if(my $old_circ = $circs->[0]) { # an open circ was found
1035 my $payload = {copy => $copy};
1037 if($old_circ->usr == $self->patron->id) {
1039 $payload->{old_circ} = $old_circ;
1041 # If there is an open circulation on the checkout item and an auto-renew
1042 # interval is defined, inform the caller that they should go
1043 # ahead and renew the item instead of warning about open circulations.
1045 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1047 'circ.checkout_auto_renew_age',
1051 if($auto_renew_intvl) {
1052 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1053 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1055 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1056 $payload->{auto_renew} = 1;
1061 return $self->bail_on_events(
1062 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1068 my $LEGACY_CIRC_EVENT_MAP = {
1069 'no_item' => 'ITEM_NOT_CATALOGED',
1070 'actor.usr.barred' => 'PATRON_BARRED',
1071 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1072 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1073 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1074 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1075 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1076 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1077 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1078 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1079 'config.circ_matrix_test.total_copy_hold_ratio' =>
1080 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
1081 'config.circ_matrix_test.available_copy_hold_ratio' =>
1082 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
1086 # ---------------------------------------------------------------------
1087 # This pushes any patron-related events into the list but does not
1088 # set bail_out for any events
1089 # ---------------------------------------------------------------------
1090 sub run_patron_permit_scripts {
1092 my $runner = $self->script_runner;
1093 my $patronid = $self->patron->id;
1097 if(!$self->legacy_script_support) {
1099 my $results = $self->run_indb_circ_test;
1100 unless($self->circ_test_success) {
1101 my @trimmed_results;
1103 if ($self->is_noncat) {
1104 # no_item result is OK during noncat checkout
1105 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1109 if ($self->checkout_is_for_hold) {
1110 # if this checkout will fulfill a hold, ignore CIRC blocks
1111 # and rely instead on the (later-checked) FULFILL block
1113 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1114 my $fblock_pens = $self->editor->search_config_standing_penalty(
1115 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1117 for my $res (@$results) {
1118 my $name = $res->{fail_part} || '';
1119 next if grep {$_->name eq $name} @$fblock_pens;
1120 push(@trimmed_results, $res);
1124 # not for hold or noncat
1125 @trimmed_results = @$results;
1129 # update the final set of test results
1130 $self->matrix_test_result(\@trimmed_results);
1132 push @allevents, $self->matrix_test_result_events;
1137 # ---------------------------------------------------------------------
1138 # # Now run the patron permit script
1139 # ---------------------------------------------------------------------
1140 $runner->load($self->circ_permit_patron);
1141 my $result = $runner->run or
1142 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1144 my $patron_events = $result->{events};
1146 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1147 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1148 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1149 $penalties = $penalties->{fatal_penalties};
1151 for my $pen (@$penalties) {
1152 # CIRC blocks are ignored if this is a FULFILL scenario
1153 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1154 my $event = OpenILS::Event->new($pen->name);
1155 $event->{desc} = $pen->label;
1156 push(@allevents, $event);
1159 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1163 $_->{payload} = $self->copy if
1164 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1167 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1169 $self->push_events(@allevents);
1172 sub matrix_test_result_codes {
1174 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1177 sub matrix_test_result_events {
1180 my $event = new OpenILS::Event(
1181 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1183 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1185 } (@{$self->matrix_test_result});
1188 sub run_indb_circ_test {
1190 return $self->matrix_test_result if $self->matrix_test_result;
1192 my $dbfunc = ($self->is_renewal) ?
1193 'action.item_user_renew_test' : 'action.item_user_circ_test';
1195 if( $self->is_precat && $self->request_precat) {
1196 $self->make_precat_copy;
1197 return if $self->bail_out;
1200 my $results = $self->editor->json_query(
1204 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1210 $self->circ_test_success($U->is_true($results->[0]->{success}));
1212 if(my $mp = $results->[0]->{matchpoint}) {
1213 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1214 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1215 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1216 if(defined($results->[0]->{renewals})) {
1217 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1219 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1220 if(defined($results->[0]->{grace_period})) {
1221 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1223 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1224 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1225 # Grab the *last* response for limit_groups, where it is more likely to be filled
1226 $self->limit_groups($results->[-1]->{limit_groups});
1229 return $self->matrix_test_result($results);
1232 # ---------------------------------------------------------------------
1233 # given a use and copy, this will calculate the circulation policy
1234 # parameters. Only works with in-db circ.
1235 # ---------------------------------------------------------------------
1239 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1241 $self->run_indb_circ_test;
1244 circ_test_success => $self->circ_test_success,
1245 failure_events => [],
1246 failure_codes => [],
1247 matchpoint => $self->circ_matrix_matchpoint
1250 unless($self->circ_test_success) {
1251 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1252 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1255 if($self->circ_matrix_matchpoint) {
1256 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1257 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1258 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1259 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1261 my $policy = $self->get_circ_policy(
1262 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1264 $$results{$_} = $$policy{$_} for keys %$policy;
1270 # ---------------------------------------------------------------------
1271 # Loads the circ policy info for duration, recurring fine, and max
1272 # fine based on the current copy
1273 # ---------------------------------------------------------------------
1274 sub get_circ_policy {
1275 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1278 duration_rule => $duration_rule->name,
1279 recurring_fine_rule => $recurring_fine_rule->name,
1280 max_fine_rule => $max_fine_rule->name,
1281 max_fine => $self->get_max_fine_amount($max_fine_rule),
1282 fine_interval => $recurring_fine_rule->recurrence_interval,
1283 renewal_remaining => $duration_rule->max_renewals,
1284 grace_period => $recurring_fine_rule->grace_period
1287 if($hard_due_date) {
1288 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1289 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1292 $policy->{duration_date_ceiling} = undef;
1293 $policy->{duration_date_ceiling_force} = undef;
1296 $policy->{duration} = $duration_rule->shrt
1297 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1298 $policy->{duration} = $duration_rule->normal
1299 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1300 $policy->{duration} = $duration_rule->extended
1301 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1303 $policy->{recurring_fine} = $recurring_fine_rule->low
1304 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1305 $policy->{recurring_fine} = $recurring_fine_rule->normal
1306 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1307 $policy->{recurring_fine} = $recurring_fine_rule->high
1308 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1313 sub get_max_fine_amount {
1315 my $max_fine_rule = shift;
1316 my $max_amount = $max_fine_rule->amount;
1318 # if is_percent is true then the max->amount is
1319 # use as a percentage of the copy price
1320 if ($U->is_true($max_fine_rule->is_percent)) {
1321 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1322 $max_amount = $price * $max_fine_rule->amount / 100;
1324 $U->ou_ancestor_setting_value(
1326 'circ.max_fine.cap_at_price',
1330 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1331 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1339 sub run_copy_permit_scripts {
1341 my $copy = $self->copy || return;
1342 my $runner = $self->script_runner;
1346 if(!$self->legacy_script_support) {
1347 my $results = $self->run_indb_circ_test;
1348 push @allevents, $self->matrix_test_result_events
1349 unless $self->circ_test_success;
1352 # ---------------------------------------------------------------------
1353 # Capture all of the copy permit events
1354 # ---------------------------------------------------------------------
1355 $runner->load($self->circ_permit_copy);
1356 my $result = $runner->run or
1357 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1358 my $copy_events = $result->{events};
1360 # ---------------------------------------------------------------------
1361 # Now collect all of the events together
1362 # ---------------------------------------------------------------------
1363 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1366 # See if this copy has an alert message
1367 my $ae = $self->check_copy_alert();
1368 push( @allevents, $ae ) if $ae;
1370 # uniquify the events
1371 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1372 @allevents = values %hash;
1374 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1376 $self->push_events(@allevents);
1380 sub check_copy_alert {
1382 return undef if $self->is_renewal;
1383 return OpenILS::Event->new(
1384 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1385 if $self->copy and $self->copy->alert_message;
1391 # --------------------------------------------------------------------------
1392 # If the call is overriding and has permissions to override every collected
1393 # event, the are cleared. Any event that the caller does not have
1394 # permission to override, will be left in the event list and bail_out will
1396 # XXX We need code in here to cancel any holds/transits on copies
1397 # that are being force-checked out
1398 # --------------------------------------------------------------------------
1399 sub override_events {
1401 my @events = @{$self->events};
1402 return unless @events;
1403 my $oargs = $self->override_args;
1405 if(!$self->override) {
1406 return $self->bail_out(1)
1407 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1412 for my $e (@events) {
1413 my $tc = $e->{textcode};
1414 next if $tc eq 'SUCCESS';
1415 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1416 my $ov = "$tc.override";
1417 $logger->info("circulator: attempting to override event: $ov");
1419 return $self->bail_on_events($self->editor->event)
1420 unless( $self->editor->allowed($ov) );
1422 return $self->bail_out(1);
1428 # --------------------------------------------------------------------------
1429 # If there is an open claimsreturn circ on the requested copy, close the
1430 # circ if overriding, otherwise bail out
1431 # --------------------------------------------------------------------------
1432 sub handle_claims_returned {
1434 my $copy = $self->copy;
1436 my $CR = $self->editor->search_action_circulation(
1438 target_copy => $copy->id,
1439 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1440 checkin_time => undef,
1444 return unless ($CR = $CR->[0]);
1448 # - If the caller has set the override flag, we will check the item in
1449 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1451 $CR->checkin_time('now');
1452 $CR->checkin_scan_time('now');
1453 $CR->checkin_lib($self->circ_lib);
1454 $CR->checkin_workstation($self->editor->requestor->wsid);
1455 $CR->checkin_staff($self->editor->requestor->id);
1457 $evt = $self->editor->event
1458 unless $self->editor->update_action_circulation($CR);
1461 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1464 $self->bail_on_events($evt) if $evt;
1469 # --------------------------------------------------------------------------
1470 # This performs the checkout
1471 # --------------------------------------------------------------------------
1475 $self->log_me("do_checkout()");
1477 # make sure perms are good if this isn't a renewal
1478 unless( $self->is_renewal ) {
1479 return $self->bail_on_events($self->editor->event)
1480 unless( $self->editor->allowed('COPY_CHECKOUT') );
1483 # verify the permit key
1484 unless( $self->check_permit_key ) {
1485 if( $self->permit_override ) {
1486 return $self->bail_on_events($self->editor->event)
1487 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1489 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1493 # if this is a non-cataloged circ, build the circ and finish
1494 if( $self->is_noncat ) {
1495 $self->checkout_noncat;
1497 OpenILS::Event->new('SUCCESS',
1498 payload => { noncat_circ => $self->circ }));
1502 if( $self->is_precat ) {
1503 $self->make_precat_copy;
1504 return if $self->bail_out;
1506 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1507 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1510 $self->do_copy_checks;
1511 return if $self->bail_out;
1513 $self->run_checkout_scripts();
1514 return if $self->bail_out;
1516 $self->build_checkout_circ_object();
1517 return if $self->bail_out;
1519 my $modify_to_start = $self->booking_adjusted_due_date();
1520 return if $self->bail_out;
1522 $self->apply_modified_due_date($modify_to_start);
1523 return if $self->bail_out;
1525 return $self->bail_on_events($self->editor->event)
1526 unless $self->editor->create_action_circulation($self->circ);
1528 # refresh the circ to force local time zone for now
1529 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1531 if($self->limit_groups) {
1532 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1535 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1537 return if $self->bail_out;
1539 $self->apply_deposit_fee();
1540 return if $self->bail_out;
1542 $self->handle_checkout_holds();
1543 return if $self->bail_out;
1545 # ------------------------------------------------------------------------------
1546 # Update the patron penalty info in the DB. Run it for permit-overrides
1547 # since the penalties are not updated during the permit phase
1548 # ------------------------------------------------------------------------------
1549 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1551 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1554 if($self->is_renewal) {
1555 # flesh the billing summary for the checked-in circ
1556 $pcirc = $self->editor->retrieve_action_circulation([
1558 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1563 OpenILS::Event->new('SUCCESS',
1565 copy => $U->unflesh_copy($self->copy),
1566 volume => $self->volume,
1567 circ => $self->circ,
1569 holds_fulfilled => $self->fulfilled_holds,
1570 deposit_billing => $self->deposit_billing,
1571 rental_billing => $self->rental_billing,
1572 parent_circ => $pcirc,
1573 patron => ($self->return_patron) ? $self->patron : undef,
1574 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1580 sub apply_deposit_fee {
1582 my $copy = $self->copy;
1584 ($self->is_deposit and not $self->is_deposit_exempt) or
1585 ($self->is_rental and not $self->is_rental_exempt);
1587 return if $self->is_deposit and $self->skip_deposit_fee;
1588 return if $self->is_rental and $self->skip_rental_fee;
1590 my $bill = Fieldmapper::money::billing->new;
1591 my $amount = $copy->deposit_amount;
1595 if($self->is_deposit) {
1596 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1598 $self->deposit_billing($bill);
1600 $billing_type = OILS_BILLING_TYPE_RENTAL;
1602 $self->rental_billing($bill);
1605 $bill->xact($self->circ->id);
1606 $bill->amount($amount);
1607 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1608 $bill->billing_type($billing_type);
1609 $bill->btype($btype);
1610 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1612 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1617 my $copy = $self->copy;
1619 my $stat = $copy->status if ref $copy->status;
1620 my $loc = $copy->location if ref $copy->location;
1621 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1623 $copy->status($stat->id) if $stat;
1624 $copy->location($loc->id) if $loc;
1625 $copy->circ_lib($circ_lib->id) if $circ_lib;
1626 $copy->editor($self->editor->requestor->id);
1627 $copy->edit_date('now');
1628 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1630 return $self->bail_on_events($self->editor->event)
1631 unless $self->editor->update_asset_copy($self->copy);
1633 $copy->status($U->copy_status($copy->status));
1634 $copy->location($loc) if $loc;
1635 $copy->circ_lib($circ_lib) if $circ_lib;
1638 sub update_reservation {
1640 my $reservation = $self->reservation;
1642 my $usr = $reservation->usr;
1643 my $target_rt = $reservation->target_resource_type;
1644 my $target_r = $reservation->target_resource;
1645 my $current_r = $reservation->current_resource;
1647 $reservation->usr($usr->id) if ref $usr;
1648 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1649 $reservation->target_resource($target_r->id) if ref $target_r;
1650 $reservation->current_resource($current_r->id) if ref $current_r;
1652 return $self->bail_on_events($self->editor->event)
1653 unless $self->editor->update_booking_reservation($self->reservation);
1656 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1657 $self->reservation($reservation);
1661 sub bail_on_events {
1662 my( $self, @evts ) = @_;
1663 $self->push_events(@evts);
1667 # ------------------------------------------------------------------------------
1668 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1669 # affects copies that will fulfill holds and CIRC affects all other copies.
1670 # If blocks exists, bail, push Events onto the event pile, and return true.
1671 # ------------------------------------------------------------------------------
1672 sub check_hold_fulfill_blocks {
1675 # See if the user has any penalties applied that prevent hold fulfillment
1676 my $pens = $self->editor->json_query({
1677 select => {csp => ['name', 'label']},
1678 from => {ausp => {csp => {}}},
1681 usr => $self->patron->id,
1682 org_unit => $U->get_org_full_path($self->circ_lib),
1684 {stop_date => undef},
1685 {stop_date => {'>' => 'now'}}
1688 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1692 return 0 unless @$pens;
1694 for my $pen (@$pens) {
1695 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1696 my $event = OpenILS::Event->new($pen->{name});
1697 $event->{desc} = $pen->{label};
1698 $self->push_events($event);
1701 $self->override_events;
1702 return $self->bail_out;
1706 # ------------------------------------------------------------------------------
1707 # When an item is checked out, see if we can fulfill a hold for this patron
1708 # ------------------------------------------------------------------------------
1709 sub handle_checkout_holds {
1711 my $copy = $self->copy;
1712 my $patron = $self->patron;
1714 my $e = $self->editor;
1715 $self->fulfilled_holds([]);
1717 # non-cats can't fulfill a hold
1718 return if $self->is_noncat;
1720 my $hold = $e->search_action_hold_request({
1721 current_copy => $copy->id ,
1722 cancel_time => undef,
1723 fulfillment_time => undef,
1725 {expire_time => undef},
1726 {expire_time => {'>' => 'now'}}
1730 if($hold and $hold->usr != $patron->id) {
1731 # reset the hold since the copy is now checked out
1733 $logger->info("circulator: un-targeting hold ".$hold->id.
1734 " because copy ".$copy->id." is getting checked out");
1736 $hold->clear_prev_check_time;
1737 $hold->clear_current_copy;
1738 $hold->clear_capture_time;
1739 $hold->clear_shelf_time;
1740 $hold->clear_shelf_expire_time;
1741 $hold->clear_current_shelf_lib;
1743 return $self->bail_on_event($e->event)
1744 unless $e->update_action_hold_request($hold);
1750 $hold = $self->find_related_user_hold($copy, $patron) or return;
1751 $logger->info("circulator: found related hold to fulfill in checkout");
1754 return if $self->check_hold_fulfill_blocks;
1756 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1758 # if the hold was never officially captured, capture it.
1759 $hold->current_copy($copy->id);
1760 $hold->capture_time('now') unless $hold->capture_time;
1761 $hold->fulfillment_time('now');
1762 $hold->fulfillment_staff($e->requestor->id);
1763 $hold->fulfillment_lib($self->circ_lib);
1765 return $self->bail_on_events($e->event)
1766 unless $e->update_action_hold_request($hold);
1768 return $self->fulfilled_holds([$hold->id]);
1772 # ------------------------------------------------------------------------------
1773 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1774 # the patron directly targets the checked out item, see if there is another hold
1775 # for the patron that could be fulfilled by the checked out item. Fulfill the
1776 # oldest hold and only fulfill 1 of them.
1778 # For "another hold":
1780 # First, check for one that the copy matches via hold_copy_map, ensuring that
1781 # *any* hold type that this copy could fill may end up filled.
1783 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1784 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1785 # that are non-requestable to count as capturing those hold types.
1786 # ------------------------------------------------------------------------------
1787 sub find_related_user_hold {
1788 my($self, $copy, $patron) = @_;
1789 my $e = $self->editor;
1791 # holds on precat copies are always copy-level, so this call will
1792 # always return undef. Exit early.
1793 return undef if $self->is_precat;
1795 return undef unless $U->ou_ancestor_setting_value(
1796 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1798 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1800 select => {ahr => ['id']},
1809 fkey => 'current_copy',
1810 type => 'left' # there may be no current_copy
1817 fulfillment_time => undef,
1818 cancel_time => undef,
1820 {expire_time => undef},
1821 {expire_time => {'>' => 'now'}}
1825 target_copy => $self->copy->id
1829 {id => undef}, # left-join copy may be nonexistent
1830 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1834 order_by => {ahr => {request_time => {direction => 'asc'}}},
1838 my $hold_info = $e->json_query($args)->[0];
1839 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1840 return undef if $U->ou_ancestor_setting_value(
1841 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1843 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1845 select => {ahr => ['id']},
1850 fkey => 'current_copy',
1851 type => 'left' # there may be no current_copy
1858 fulfillment_time => undef,
1859 cancel_time => undef,
1861 {expire_time => undef},
1862 {expire_time => {'>' => 'now'}}
1869 target => $self->volume->id
1875 target => $self->title->id
1881 {id => undef}, # left-join copy may be nonexistent
1882 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1886 order_by => {ahr => {request_time => {direction => 'asc'}}},
1890 $hold_info = $e->json_query($args)->[0];
1891 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1896 sub run_checkout_scripts {
1901 my $runner = $self->script_runner;
1910 my $hard_due_date_name;
1912 if(!$self->legacy_script_support) {
1913 $self->run_indb_circ_test();
1914 $duration = $self->circ_matrix_matchpoint->duration_rule;
1915 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1916 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1917 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1921 $runner->load($self->circ_duration);
1923 my $result = $runner->run or
1924 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1926 $duration_name = $result->{durationRule};
1927 $recurring_name = $result->{recurringFinesRule};
1928 $max_fine_name = $result->{maxFine};
1929 $hard_due_date_name = $result->{hardDueDate};
1932 $duration_name = $duration->name if $duration;
1933 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1936 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1937 return $self->bail_on_events($evt) if ($evt && !$nobail);
1939 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1940 return $self->bail_on_events($evt) if ($evt && !$nobail);
1942 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1943 return $self->bail_on_events($evt) if ($evt && !$nobail);
1945 if($hard_due_date_name) {
1946 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1947 return $self->bail_on_events($evt) if ($evt && !$nobail);
1953 # The item circulates with an unlimited duration
1957 $hard_due_date = undef;
1960 $self->duration_rule($duration);
1961 $self->recurring_fines_rule($recurring);
1962 $self->max_fine_rule($max_fine);
1963 $self->hard_due_date($hard_due_date);
1967 sub build_checkout_circ_object {
1970 my $circ = Fieldmapper::action::circulation->new;
1971 my $duration = $self->duration_rule;
1972 my $max = $self->max_fine_rule;
1973 my $recurring = $self->recurring_fines_rule;
1974 my $hard_due_date = $self->hard_due_date;
1975 my $copy = $self->copy;
1976 my $patron = $self->patron;
1977 my $duration_date_ceiling;
1978 my $duration_date_ceiling_force;
1982 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1983 $duration_date_ceiling = $policy->{duration_date_ceiling};
1984 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1986 my $dname = $duration->name;
1987 my $mname = $max->name;
1988 my $rname = $recurring->name;
1990 if($hard_due_date) {
1991 $hdname = $hard_due_date->name;
1994 $logger->debug("circulator: building circulation ".
1995 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1997 $circ->duration($policy->{duration});
1998 $circ->recurring_fine($policy->{recurring_fine});
1999 $circ->duration_rule($duration->name);
2000 $circ->recurring_fine_rule($recurring->name);
2001 $circ->max_fine_rule($max->name);
2002 $circ->max_fine($policy->{max_fine});
2003 $circ->fine_interval($recurring->recurrence_interval);
2004 $circ->renewal_remaining($duration->max_renewals);
2005 $circ->grace_period($policy->{grace_period});
2009 $logger->info("circulator: copy found with an unlimited circ duration");
2010 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2011 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2012 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2013 $circ->renewal_remaining(0);
2014 $circ->grace_period(0);
2017 $circ->target_copy( $copy->id );
2018 $circ->usr( $patron->id );
2019 $circ->circ_lib( $self->circ_lib );
2020 $circ->workstation($self->editor->requestor->wsid)
2021 if defined $self->editor->requestor->wsid;
2023 # renewals maintain a link to the parent circulation
2024 $circ->parent_circ($self->parent_circ);
2026 if( $self->is_renewal ) {
2027 $circ->opac_renewal('t') if $self->opac_renewal;
2028 $circ->phone_renewal('t') if $self->phone_renewal;
2029 $circ->desk_renewal('t') if $self->desk_renewal;
2030 $circ->renewal_remaining($self->renewal_remaining);
2031 $circ->circ_staff($self->editor->requestor->id);
2035 # if the user provided an overiding checkout time,
2036 # (e.g. the checkout really happened several hours ago), then
2037 # we apply that here. Does this need a perm??
2038 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2039 if $self->checkout_time;
2041 # if a patron is renewing, 'requestor' will be the patron
2042 $circ->circ_staff($self->editor->requestor->id);
2043 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2048 sub do_reservation_pickup {
2051 $self->log_me("do_reservation_pickup()");
2053 $self->reservation->pickup_time('now');
2056 $self->reservation->current_resource &&
2057 $U->is_true($self->reservation->target_resource_type->catalog_item)
2059 # We used to try to set $self->copy and $self->patron here,
2060 # but that should already be done.
2062 $self->run_checkout_scripts(1);
2064 my $duration = $self->duration_rule;
2065 my $max = $self->max_fine_rule;
2066 my $recurring = $self->recurring_fines_rule;
2068 if ($duration && $max && $recurring) {
2069 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2071 my $dname = $duration->name;
2072 my $mname = $max->name;
2073 my $rname = $recurring->name;
2075 $logger->debug("circulator: updating reservation ".
2076 "with duration=$dname, maxfine=$mname, recurring=$rname");
2078 $self->reservation->fine_amount($policy->{recurring_fine});
2079 $self->reservation->max_fine($policy->{max_fine});
2080 $self->reservation->fine_interval($recurring->recurrence_interval);
2083 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2084 $self->update_copy();
2087 $self->reservation->fine_amount(
2088 $self->reservation->target_resource_type->fine_amount
2090 $self->reservation->max_fine(
2091 $self->reservation->target_resource_type->max_fine
2093 $self->reservation->fine_interval(
2094 $self->reservation->target_resource_type->fine_interval
2098 $self->update_reservation();
2101 sub do_reservation_return {
2103 my $request = shift;
2105 $self->log_me("do_reservation_return()");
2107 if (not ref $self->reservation) {
2108 my ($reservation, $evt) =
2109 $U->fetch_booking_reservation($self->reservation);
2110 return $self->bail_on_events($evt) if $evt;
2111 $self->reservation($reservation);
2114 $self->handle_fines(1);
2115 $self->reservation->return_time('now');
2116 $self->update_reservation();
2117 $self->reshelve_copy if $self->copy;
2119 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2120 $self->copy( $self->reservation->current_resource->catalog_item );
2124 sub booking_adjusted_due_date {
2126 my $circ = $self->circ;
2127 my $copy = $self->copy;
2129 return undef unless $self->use_booking;
2133 if( $self->due_date ) {
2135 return $self->bail_on_events($self->editor->event)
2136 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2138 $circ->due_date(cleanse_ISO8601($self->due_date));
2142 return unless $copy and $circ->due_date;
2145 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2146 if (@$booking_items) {
2147 my $booking_item = $booking_items->[0];
2148 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2150 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2151 my $shorten_circ_setting = $resource_type->elbow_room ||
2152 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2155 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2156 my $bookings = $booking_ses->request(
2157 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2158 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2160 $booking_ses->disconnect;
2162 my $dt_parser = DateTime::Format::ISO8601->new;
2163 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2165 for my $bid (@$bookings) {
2167 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2169 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2170 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2172 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2173 if ($booking_start < DateTime->now);
2176 if ($U->is_true($stop_circ_setting)) {
2177 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2179 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2180 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2183 # We set the circ duration here only to affect the logic that will
2184 # later (in a DB trigger) mangle the time part of the due date to
2185 # 11:59pm. Having any circ duration that is not a whole number of
2186 # days is enough to prevent the "correction."
2187 my $new_circ_duration = $due_date->epoch - time;
2188 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2189 $circ->duration("$new_circ_duration seconds");
2191 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2195 return $self->bail_on_events($self->editor->event)
2196 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2202 sub apply_modified_due_date {
2204 my $shift_earlier = shift;
2205 my $circ = $self->circ;
2206 my $copy = $self->copy;
2208 if( $self->due_date ) {
2210 return $self->bail_on_events($self->editor->event)
2211 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2213 $circ->due_date(cleanse_ISO8601($self->due_date));
2217 # if the due_date lands on a day when the location is closed
2218 return unless $copy and $circ->due_date;
2220 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2222 # due-date overlap should be determined by the location the item
2223 # is checked out from, not the owning or circ lib of the item
2224 my $org = $self->circ_lib;
2226 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2227 " with an item due date of ".$circ->due_date );
2229 my $dateinfo = $U->storagereq(
2230 'open-ils.storage.actor.org_unit.closed_date.overlap',
2231 $org, $circ->due_date );
2234 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2235 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2237 # XXX make the behavior more dynamic
2238 # for now, we just push the due date to after the close date
2239 if ($shift_earlier) {
2240 $circ->due_date($dateinfo->{start});
2242 $circ->due_date($dateinfo->{end});
2250 sub create_due_date {
2251 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2253 # if there is a raw time component (e.g. from postgres),
2254 # turn it into an interval that interval_to_seconds can parse
2255 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2257 # for now, use the server timezone. TODO: use workstation org timezone
2258 my $due_date = DateTime->now(time_zone => 'local');
2259 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2261 # add the circ duration
2262 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2265 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2266 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2267 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2272 # return ISO8601 time with timezone
2273 return $due_date->strftime('%FT%T%z');
2278 sub make_precat_copy {
2280 my $copy = $self->copy;
2283 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2285 $copy->editor($self->editor->requestor->id);
2286 $copy->edit_date('now');
2287 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2288 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2289 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2290 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2291 $self->update_copy();
2295 $logger->info("circulator: Creating a new precataloged ".
2296 "copy in checkout with barcode " . $self->copy_barcode);
2298 $copy = Fieldmapper::asset::copy->new;
2299 $copy->circ_lib($self->circ_lib);
2300 $copy->creator($self->editor->requestor->id);
2301 $copy->editor($self->editor->requestor->id);
2302 $copy->barcode($self->copy_barcode);
2303 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2304 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2305 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2307 $copy->dummy_title($self->dummy_title || "");
2308 $copy->dummy_author($self->dummy_author || "");
2309 $copy->dummy_isbn($self->dummy_isbn || "");
2310 $copy->circ_modifier($self->circ_modifier);
2313 # See if we need to override the circ_lib for the copy with a configured circ_lib
2314 # Setting is shortname of the org unit
2315 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2316 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2318 if($precat_circ_lib) {
2319 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2322 $self->bail_on_events($self->editor->event);
2326 $copy->circ_lib($org->id);
2330 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2332 $self->push_events($self->editor->event);
2336 # this is a little bit of a hack, but we need to
2337 # get the copy into the script runner
2338 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2342 sub checkout_noncat {
2348 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2349 my $count = $self->noncat_count || 1;
2350 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2352 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2356 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2357 $self->editor->requestor->id,
2365 $self->push_events($evt);
2373 # If a copy goes into transit and is then checked in before the transit checkin
2374 # interval has expired, push an event onto the overridable events list.
2375 sub check_transit_checkin_interval {
2378 # only concerned with in-transit items
2379 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2381 # no interval, no problem
2382 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2383 return unless $interval;
2385 # capture the transit so we don't have to fetch it again later during checkin
2387 $self->editor->search_action_transit_copy(
2388 {target_copy => $self->copy->id, dest_recv_time => undef}
2392 # transit from X to X for whatever reason has no min interval
2393 return if $self->transit->source == $self->transit->dest;
2395 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2396 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2397 my $horizon = $t_start->add(seconds => $seconds);
2399 # See if we are still within the transit checkin forbidden range
2400 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2401 if $horizon > DateTime->now;
2404 # Retarget local holds at checkin
2405 sub checkin_retarget {
2407 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2408 return unless $self->is_checkin; # Renewals need not be checked
2409 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2410 return if $self->is_precat; # No holds for precats
2411 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2412 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2413 my $status = $U->copy_status($self->copy->status);
2414 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2415 # Specifically target items that are likely new (by status ID)
2416 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2417 my $location = $self->copy->location;
2418 if(!ref($location)) {
2419 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2420 $self->copy->location($location);
2422 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2424 # Fetch holds for the bib
2425 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2426 $self->editor->authtoken,
2429 capture_time => undef, # No touching captured holds
2430 frozen => 'f', # Don't bother with frozen holds
2431 pickup_lib => $self->circ_lib # Only holds actually here
2434 # Error? Skip the step.
2435 return if exists $result->{"ilsevent"};
2439 foreach my $holdlist (keys %{$result}) {
2440 push @$holds, @{$result->{$holdlist}};
2443 return if scalar(@$holds) == 0; # No holds, no retargeting
2445 # Check for parts on this copy
2446 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2447 my %parts_hash = ();
2448 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2450 # Loop over holds in request-ish order
2451 # Stage 1: Get them into request-ish order
2452 # Also grab type and target for skipping low hanging ones
2453 $result = $self->editor->json_query({
2454 "select" => { "ahr" => ["id", "hold_type", "target"] },
2455 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2456 "where" => { "id" => $holds },
2458 { "class" => "pgt", "field" => "hold_priority"},
2459 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2460 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2461 { "class" => "ahr", "field" => "request_time"}
2466 if (ref $result eq "ARRAY" and scalar @$result) {
2467 foreach (@{$result}) {
2468 # Copy level, but not this copy?
2469 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2470 and $_->{target} != $self->copy->id);
2471 # Volume level, but not this volume?
2472 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2473 if(@$parts) { # We have parts?
2475 next if ($_->{hold_type} eq 'T');
2476 # Skip part holds for parts not on this copy
2477 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2479 # No parts, no part holds
2480 next if ($_->{hold_type} eq 'P');
2482 # So much for easy stuff, attempt a retarget!
2483 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2484 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2485 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2493 $self->log_me("do_checkin()");
2495 return $self->bail_on_events(
2496 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2499 $self->check_transit_checkin_interval;
2500 $self->checkin_retarget;
2502 # the renew code and mk_env should have already found our circulation object
2503 unless( $self->circ ) {
2505 my $circs = $self->editor->search_action_circulation(
2506 { target_copy => $self->copy->id, checkin_time => undef });
2508 $self->circ($$circs[0]);
2510 # for now, just warn if there are multiple open circs on a copy
2511 $logger->warn("circulator: we have ".scalar(@$circs).
2512 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2515 my $stat = $U->copy_status($self->copy->status)->id;
2517 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2518 # differently if they are already paid for. We need to check for this
2519 # early since overdue generation is potentially affected.
2520 my $dont_change_lost_zero = 0;
2521 if ($stat == OILS_COPY_STATUS_LOST
2522 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2523 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2525 # LOST fine settings are controlled by the copy's circ lib, not the the
2527 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2528 $self->copy->circ_lib->id : $self->copy->circ_lib;
2529 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2530 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2531 $self->editor) || 0;
2533 if ($dont_change_lost_zero) {
2534 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2535 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2538 $self->dont_change_lost_zero($dont_change_lost_zero);
2541 my $ignore_stop_fines = undef;
2542 if ($self->circ and !$dont_change_lost_zero) {
2544 # if this circ is LOST and we are configured to generate overdue
2545 # fines for lost items on checkin (to fill the gap between mark
2546 # lost time and when the fines would have naturally stopped), tell
2547 # the fine generator to ignore the stop-fines value on this circ.
2548 # XXX should this setting come from the copy circ lib (like other
2549 # LOST settings), instead of the circulation circ lib?
2550 if ($stat == OILS_COPY_STATUS_LOST) {
2551 $ignore_stop_fines = $self->circ->stop_fines if
2552 $U->ou_ancestor_setting_value(
2554 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2559 # run the fine generator against this circ
2560 $self->handle_fines(undef, $ignore_stop_fines);
2563 if( $self->checkin_check_holds_shelf() ) {
2564 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2565 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2566 if($self->fake_hold_dest) {
2567 $self->hold->pickup_lib($self->circ_lib);
2569 $self->checkin_flesh_events;
2573 unless( $self->is_renewal ) {
2574 return $self->bail_on_events($self->editor->event)
2575 unless $self->editor->allowed('COPY_CHECKIN');
2578 $self->push_events($self->check_copy_alert());
2579 $self->push_events($self->check_checkin_copy_status());
2581 # if the circ is marked as 'claims returned', add the event to the list
2582 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2583 if ($self->circ and $self->circ->stop_fines
2584 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2586 $self->check_circ_deposit();
2588 # handle the overridable events
2589 $self->override_events unless $self->is_renewal;
2590 return if $self->bail_out;
2592 if( $self->copy and !$self->transit ) {
2594 $self->editor->search_action_transit_copy(
2595 { target_copy => $self->copy->id, dest_recv_time => undef }
2601 $self->checkin_handle_circ;
2602 return if $self->bail_out;
2603 $self->checkin_changed(1);
2605 } elsif( $self->transit ) {
2606 my $hold_transit = $self->process_received_transit;
2607 $self->checkin_changed(1);
2609 if( $self->bail_out ) {
2610 $self->checkin_flesh_events;
2614 if( my $e = $self->check_checkin_copy_status() ) {
2615 # If the original copy status is special, alert the caller
2616 my $ev = $self->events;
2617 $self->events([$e]);
2618 $self->override_events;
2619 return if $self->bail_out;
2623 if( $hold_transit or
2624 $U->copy_status($self->copy->status)->id
2625 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2628 if( $hold_transit ) {
2629 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2631 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2636 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2638 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2639 $self->reshelve_copy(1);
2640 $self->cancelled_hold_transit(1);
2641 $self->notify_hold(0); # don't notify for cancelled holds
2642 $self->fake_hold_dest(0);
2643 return if $self->bail_out;
2645 } elsif ($hold and $hold->hold_type eq 'R') {
2647 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2648 $self->notify_hold(0); # No need to notify
2649 $self->fake_hold_dest(0);
2650 $self->noop(1); # Don't try and capture for other holds/transits now
2651 $self->update_copy();
2652 $hold->fulfillment_time('now');
2653 $self->bail_on_events($self->editor->event)
2654 unless $self->editor->update_action_hold_request($hold);
2658 # hold transited to correct location
2659 if($self->fake_hold_dest) {
2660 $hold->pickup_lib($self->circ_lib);
2662 $self->checkin_flesh_events;
2667 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2669 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2670 " that is in-transit, but there is no transit.. repairing");
2671 $self->reshelve_copy(1);
2672 return if $self->bail_out;
2675 if( $self->is_renewal ) {
2676 $self->finish_fines_and_voiding;
2677 return if $self->bail_out;
2678 $self->push_events(OpenILS::Event->new('SUCCESS'));
2682 # ------------------------------------------------------------------------------
2683 # Circulations and transits are now closed where necessary. Now go on to see if
2684 # this copy can fulfill a hold or needs to be routed to a different location
2685 # ------------------------------------------------------------------------------
2687 my $needed_for_something = 0; # formerly "needed_for_hold"
2689 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2691 if (!$self->remote_hold) {
2692 if ($self->use_booking) {
2693 my $potential_hold = $self->hold_capture_is_possible;
2694 my $potential_reservation = $self->reservation_capture_is_possible;
2696 if ($potential_hold and $potential_reservation) {
2697 $logger->info("circulator: item could fulfill either hold or reservation");
2698 $self->push_events(new OpenILS::Event(
2699 "HOLD_RESERVATION_CONFLICT",
2700 "hold" => $potential_hold,
2701 "reservation" => $potential_reservation
2703 return if $self->bail_out;
2704 } elsif ($potential_hold) {
2705 $needed_for_something =
2706 $self->attempt_checkin_hold_capture;
2707 } elsif ($potential_reservation) {
2708 $needed_for_something =
2709 $self->attempt_checkin_reservation_capture;
2712 $needed_for_something = $self->attempt_checkin_hold_capture;
2715 return if $self->bail_out;
2717 unless($needed_for_something) {
2718 my $circ_lib = (ref $self->copy->circ_lib) ?
2719 $self->copy->circ_lib->id : $self->copy->circ_lib;
2721 if( $self->remote_hold ) {
2722 $circ_lib = $self->remote_hold->pickup_lib;
2723 $logger->warn("circulator: Copy ".$self->copy->barcode.
2724 " is on a remote hold's shelf, sending to $circ_lib");
2727 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2729 my $suppress_transit = 0;
2731 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2732 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2733 if($suppress_transit_source && $suppress_transit_source->{value}) {
2734 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2735 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2736 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2737 $suppress_transit = 1;
2742 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2743 # copy is where it needs to be, either for hold or reshelving
2745 $self->checkin_handle_precat();
2746 return if $self->bail_out;
2749 # copy needs to transit "home", or stick here if it's a floating copy
2751 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2752 my $res = $self->editor->json_query(
2754 'evergreen.can_float',
2755 $self->copy->floating->id,
2756 $self->copy->circ_lib,
2761 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2763 if ($can_float) { # Yep, floating, stick here
2764 $self->checkin_changed(1);
2765 $self->copy->circ_lib( $self->circ_lib );
2768 my $bc = $self->copy->barcode;
2769 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2770 $self->checkin_build_copy_transit($circ_lib);
2771 return if $self->bail_out;
2772 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2776 } else { # no-op checkin
2777 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2778 $self->checkin_changed(1);
2779 $self->copy->circ_lib( $self->circ_lib );
2784 if($self->claims_never_checked_out and
2785 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2787 # the item was not supposed to be checked out to the user and should now be marked as missing
2788 $self->copy->status(OILS_COPY_STATUS_MISSING);
2792 $self->reshelve_copy unless $needed_for_something;
2795 return if $self->bail_out;
2797 unless($self->checkin_changed) {
2799 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2800 my $stat = $U->copy_status($self->copy->status)->id;
2802 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2803 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2804 $self->bail_out(1); # no need to commit anything
2808 $self->push_events(OpenILS::Event->new('SUCCESS'))
2809 unless @{$self->events};
2812 $self->finish_fines_and_voiding;
2814 OpenILS::Utils::Penalty->calculate_penalties(
2815 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2817 $self->checkin_flesh_events;
2821 sub finish_fines_and_voiding {
2823 return unless $self->circ;
2825 return unless $self->backdate or $self->void_overdues;
2827 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2828 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2830 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2831 $self->editor, $self->circ, $self->backdate, $note);
2833 return $self->bail_on_events($evt) if $evt;
2835 # Make sure the circ is open or closed as necessary.
2836 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2837 return $self->bail_on_events($evt) if $evt;
2843 # if a deposit was payed for this item, push the event
2844 sub check_circ_deposit {
2846 return unless $self->circ;
2847 my $deposit = $self->editor->search_money_billing(
2849 xact => $self->circ->id,
2851 }, {idlist => 1})->[0];
2853 $self->push_events(OpenILS::Event->new(
2854 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2859 my $force = $self->force || shift;
2860 my $copy = $self->copy;
2862 my $stat = $U->copy_status($copy->status)->id;
2865 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2866 $stat != OILS_COPY_STATUS_CATALOGING and
2867 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2868 $stat != OILS_COPY_STATUS_RESHELVING )) {
2870 $copy->status( OILS_COPY_STATUS_RESHELVING );
2872 $self->checkin_changed(1);
2877 # Returns true if the item is at the current location
2878 # because it was transited there for a hold and the
2879 # hold has not been fulfilled
2880 sub checkin_check_holds_shelf {
2882 return 0 unless $self->copy;
2885 $U->copy_status($self->copy->status)->id ==
2886 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2888 # Attempt to clear shelf expired holds for this copy
2889 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2890 if($self->clear_expired);
2892 # find the hold that put us on the holds shelf
2893 my $holds = $self->editor->search_action_hold_request(
2895 current_copy => $self->copy->id,
2896 capture_time => { '!=' => undef },
2897 fulfillment_time => undef,
2898 cancel_time => undef,
2903 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2904 $self->reshelve_copy(1);
2908 my $hold = $$holds[0];
2910 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2911 $hold->id. "] for copy ".$self->copy->barcode);
2913 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2914 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2915 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2916 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2917 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2918 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2919 $self->fake_hold_dest(1);
2925 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2926 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2930 $logger->info("circulator: hold is not for here..");
2931 $self->remote_hold($hold);
2936 sub checkin_handle_precat {
2938 my $copy = $self->copy;
2940 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2941 $copy->status(OILS_COPY_STATUS_CATALOGING);
2942 $self->update_copy();
2943 $self->checkin_changed(1);
2944 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2949 sub checkin_build_copy_transit {
2952 my $copy = $self->copy;
2953 my $transit = Fieldmapper::action::transit_copy->new;
2955 # if we are transiting an item to the shelf shelf, it's a hold transit
2956 if (my $hold = $self->remote_hold) {
2957 $transit = Fieldmapper::action::hold_transit_copy->new;
2958 $transit->hold($hold->id);
2960 # the item is going into transit, remove any shelf-iness
2961 if ($hold->current_shelf_lib or $hold->shelf_time) {
2962 $hold->clear_current_shelf_lib;
2963 $hold->clear_shelf_time;
2964 return $self->bail_on_events($self->editor->event)
2965 unless $self->editor->update_action_hold_request($hold);
2969 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2970 $logger->info("circulator: transiting copy to $dest");
2972 $transit->source($self->circ_lib);
2973 $transit->dest($dest);
2974 $transit->target_copy($copy->id);
2975 $transit->source_send_time('now');
2976 $transit->copy_status( $U->copy_status($copy->status)->id );
2978 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2980 if ($self->remote_hold) {
2981 return $self->bail_on_events($self->editor->event)
2982 unless $self->editor->create_action_hold_transit_copy($transit);
2984 return $self->bail_on_events($self->editor->event)
2985 unless $self->editor->create_action_transit_copy($transit);
2988 # ensure the transit is returned to the caller
2989 $self->transit($transit);
2991 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2993 $self->checkin_changed(1);
2997 sub hold_capture_is_possible {
2999 my $copy = $self->copy;
3001 # we've been explicitly told not to capture any holds
3002 return 0 if $self->capture eq 'nocapture';
3004 # See if this copy can fulfill any holds
3005 my $hold = $holdcode->find_nearest_permitted_hold(
3006 $self->editor, $copy, $self->editor->requestor, 1 # check_only
3008 return undef if ref $hold eq "HASH" and
3009 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
3013 sub reservation_capture_is_possible {
3015 my $copy = $self->copy;
3017 # we've been explicitly told not to capture any holds
3018 return 0 if $self->capture eq 'nocapture';
3020 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3021 my $resv = $booking_ses->request(
3022 "open-ils.booking.reservations.could_capture",
3023 $self->editor->authtoken, $copy->barcode
3025 $booking_ses->disconnect;
3026 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
3027 $self->push_events($resv);
3033 # returns true if the item was used (or may potentially be used
3034 # in subsequent calls) to capture a hold.
3035 sub attempt_checkin_hold_capture {
3037 my $copy = $self->copy;
3039 # we've been explicitly told not to capture any holds
3040 return 0 if $self->capture eq 'nocapture';
3042 # See if this copy can fulfill any holds
3043 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3044 $self->editor, $copy, $self->editor->requestor );
3047 $logger->debug("circulator: no potential permitted".
3048 "holds found for copy ".$copy->barcode);
3052 if($self->capture ne 'capture') {
3053 # see if this item is in a hold-capture-delay location
3054 my $location = $self->copy->location;
3055 if(!ref($location)) {
3056 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3057 $self->copy->location($location);
3059 if($U->is_true($location->hold_verify)) {
3060 $self->bail_on_events(
3061 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3066 $self->retarget($retarget);
3068 my $suppress_transit = 0;
3069 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3070 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3071 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3072 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3073 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3074 $suppress_transit = 1;
3075 $hold->pickup_lib($self->circ_lib);
3080 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3082 $hold->current_copy($copy->id);
3083 $hold->capture_time('now');
3084 $self->put_hold_on_shelf($hold)
3085 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3087 # prevent DB errors caused by fetching
3088 # holds from storage, and updating through cstore
3089 $hold->clear_fulfillment_time;
3090 $hold->clear_fulfillment_staff;
3091 $hold->clear_fulfillment_lib;
3092 $hold->clear_expire_time;
3093 $hold->clear_cancel_time;
3094 $hold->clear_prev_check_time unless $hold->prev_check_time;
3096 $self->bail_on_events($self->editor->event)
3097 unless $self->editor->update_action_hold_request($hold);
3099 $self->checkin_changed(1);
3101 return 0 if $self->bail_out;
3103 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3105 if ($hold->hold_type eq 'R') {
3106 $copy->status(OILS_COPY_STATUS_CATALOGING);
3107 $hold->fulfillment_time('now');
3108 $self->noop(1); # Block other transit/hold checks
3109 $self->bail_on_events($self->editor->event)
3110 unless $self->editor->update_action_hold_request($hold);
3112 # This hold was captured in the correct location
3113 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3114 $self->push_events(OpenILS::Event->new('SUCCESS'));
3116 #$self->do_hold_notify($hold->id);
3117 $self->notify_hold($hold->id);
3122 # Hold needs to be picked up elsewhere. Build a hold
3123 # transit and route the item.
3124 $self->checkin_build_hold_transit();
3125 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3126 return 0 if $self->bail_out;
3127 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3130 # make sure we save the copy status
3132 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3136 sub attempt_checkin_reservation_capture {
3138 my $copy = $self->copy;
3140 # we've been explicitly told not to capture any holds
3141 return 0 if $self->capture eq 'nocapture';
3143 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3144 my $evt = $booking_ses->request(
3145 "open-ils.booking.resources.capture_for_reservation",
3146 $self->editor->authtoken,
3148 1 # don't update copy - we probably have it locked
3150 $booking_ses->disconnect;
3152 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3154 "open-ils.booking.resources.capture_for_reservation " .
3155 "didn't return an event!"
3159 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3160 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3162 # not-transferable is an error event we'll pass on the user
3163 $logger->warn("reservation capture attempted against non-transferable item");
3164 $self->push_events($evt);
3166 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3167 # Re-retrieve copy as reservation capture may have changed
3168 # its status and whatnot.
3170 "circulator: booking capture win on copy " . $self->copy->id
3172 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3174 "circulator: changing copy " . $self->copy->id .
3175 "'s status from " . $self->copy->status . " to " .
3178 $self->copy->status($new_copy_status);
3181 $self->reservation($evt->{"payload"}->{"reservation"});
3183 if (exists $evt->{"payload"}->{"transit"}) {
3187 "org" => $evt->{"payload"}->{"transit"}->dest
3191 $self->checkin_changed(1);
3195 # other results are treated as "nothing to capture"
3199 sub do_hold_notify {
3200 my( $self, $holdid ) = @_;
3202 my $e = new_editor(xact => 1);
3203 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3205 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3206 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3208 $logger->info("circulator: running delayed hold notify process");
3210 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3211 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3213 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3214 hold_id => $holdid, requestor => $self->editor->requestor);
3216 $logger->debug("circulator: built hold notifier");
3218 if(!$notifier->event) {
3220 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3222 my $stat = $notifier->send_email_notify;
3223 if( $stat == '1' ) {
3224 $logger->info("circulator: hold notify succeeded for hold $holdid");
3228 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3231 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3235 sub retarget_holds {
3237 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3238 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3239 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3240 # no reason to wait for the return value
3244 sub checkin_build_hold_transit {
3247 my $copy = $self->copy;
3248 my $hold = $self->hold;
3249 my $trans = Fieldmapper::action::hold_transit_copy->new;
3251 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3253 $trans->hold($hold->id);
3254 $trans->source($self->circ_lib);
3255 $trans->dest($hold->pickup_lib);
3256 $trans->source_send_time("now");
3257 $trans->target_copy($copy->id);
3259 # when the copy gets to its destination, it will recover
3260 # this status - put it onto the holds shelf
3261 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3263 return $self->bail_on_events($self->editor->event)
3264 unless $self->editor->create_action_hold_transit_copy($trans);
3269 sub process_received_transit {
3271 my $copy = $self->copy;
3272 my $copyid = $self->copy->id;
3274 my $status_name = $U->copy_status($copy->status)->name;
3275 $logger->debug("circulator: attempting transit receive on ".
3276 "copy $copyid. Copy status is $status_name");
3278 my $transit = $self->transit;
3280 # Check if we are in a transit suppress range
3281 my $suppress_transit = 0;
3282 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3283 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3284 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3285 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3286 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3287 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3288 $suppress_transit = 1;
3289 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3293 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3294 # - this item is in-transit to a different location
3295 # - Or we are capturing holds as transits, so why create a new transit?
3297 my $tid = $transit->id;
3298 my $loc = $self->circ_lib;
3299 my $dest = $transit->dest;
3301 $logger->info("circulator: Fowarding transit on copy which is destined ".
3302 "for a different location. transit=$tid, copy=$copyid, current ".
3303 "location=$loc, destination location=$dest");
3305 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3307 # grab the associated hold object if available
3308 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3309 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3311 return $self->bail_on_events($evt);
3314 # The transit is received, set the receive time
3315 $transit->dest_recv_time('now');
3316 $self->bail_on_events($self->editor->event)
3317 unless $self->editor->update_action_transit_copy($transit);
3319 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3321 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3322 $copy->status( $transit->copy_status );
3323 $self->update_copy();
3324 return if $self->bail_out;
3328 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3331 # hold has arrived at destination, set shelf time
3332 $self->put_hold_on_shelf($hold);
3333 $self->bail_on_events($self->editor->event)
3334 unless $self->editor->update_action_hold_request($hold);
3335 return if $self->bail_out;
3337 $self->notify_hold($hold_transit->hold);
3340 $hold_transit = undef;
3341 $self->cancelled_hold_transit(1);
3342 $self->reshelve_copy(1);
3343 $self->fake_hold_dest(0);
3348 OpenILS::Event->new(
3351 payload => { transit => $transit, holdtransit => $hold_transit } ));
3353 return $hold_transit;
3357 # ------------------------------------------------------------------
3358 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3359 # ------------------------------------------------------------------
3360 sub put_hold_on_shelf {
3361 my($self, $hold) = @_;
3362 $hold->shelf_time('now');
3363 $hold->current_shelf_lib($self->circ_lib);
3364 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3372 my $reservation = shift;
3373 my $ignore_stop_fines = shift;
3374 my $dt_parser = DateTime::Format::ISO8601->new;
3376 my $obj = $reservation ? $self->reservation : $self->circ;
3378 return undef if (!$ignore_stop_fines and $obj->stop_fines);
3380 # If we have a grace period
3381 if($obj->can('grace_period')) {
3382 # Parse out the due date
3383 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3384 # Add the grace period to the due date
3385 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3386 # Don't generate fines on circs still in grace period
3387 return undef if ($due_date > DateTime->now);
3390 $CC->generate_fines({circs => [$obj], editor => $self->editor});
3395 sub checkin_handle_circ {
3397 my $circ = $self->circ;
3398 my $copy = $self->copy;
3402 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3404 # backdate the circ if necessary
3405 if($self->backdate) {
3406 my $evt = $self->checkin_handle_backdate;
3407 return $self->bail_on_events($evt) if $evt;
3410 if(!$circ->stop_fines) {
3411 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3412 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3413 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3414 $circ->stop_fines_time('now');
3415 $circ->stop_fines_time($self->backdate) if $self->backdate;
3418 # Set the checkin vars since we have the item
3419 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3421 # capture the true scan time for back-dated checkins
3422 $circ->checkin_scan_time('now');
3424 $circ->checkin_staff($self->editor->requestor->id);
3425 $circ->checkin_lib($self->circ_lib);
3426 $circ->checkin_workstation($self->editor->requestor->wsid);
3428 my $circ_lib = (ref $self->copy->circ_lib) ?
3429 $self->copy->circ_lib->id : $self->copy->circ_lib;
3430 my $stat = $U->copy_status($self->copy->status)->id;
3432 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3433 # we will now handle lost fines, but the copy will retain its 'lost'
3434 # status if it needs to transit home unless lost_immediately_available
3437 # if we decide to also delay fine handling until the item arrives home,
3438 # we will need to call lost fine handling code both when checking items
3439 # in and also when receiving transits
3440 $self->checkin_handle_lost($circ_lib);
3441 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3442 # same process as above.
3443 $self->checkin_handle_long_overdue($circ_lib);
3444 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3445 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3447 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3452 # see if there are any fines owed on this circ. if not, close it
3453 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3454 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3456 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3458 return $self->bail_on_events($self->editor->event)
3459 unless $self->editor->update_action_circulation($circ);
3464 # ------------------------------------------------------------------
3465 # See if we need to void billings, etc. for lost checkin
3466 # ------------------------------------------------------------------
3467 sub checkin_handle_lost {
3469 my $circ_lib = shift;
3471 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3472 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3474 return $self->checkin_handle_lost_or_longoverdue(
3475 circ_lib => $circ_lib,
3476 max_return => $max_return,
3477 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3478 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3479 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3480 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3481 ous_use_last_activity => undef, # not supported for LOST checkin
3482 void_cost_btype => 3,
3487 # ------------------------------------------------------------------
3488 # See if we need to void billings, etc. for long-overdue checkin
3489 # note: not using constants below since they serve little purpose
3490 # for single-use strings that are descriptive in their own right
3491 # and mostly just complicate debugging.
3492 # ------------------------------------------------------------------
3493 sub checkin_handle_long_overdue {
3495 my $circ_lib = shift;
3497 $logger->info("circulator: processing long-overdue checkin...");
3499 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3500 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3502 return $self->checkin_handle_lost_or_longoverdue(
3503 circ_lib => $circ_lib,
3504 max_return => $max_return,
3505 is_longoverdue => 1,
3506 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3507 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3508 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3509 ous_immediately_available => 'circ.longoverdue_immediately_available',
3510 ous_use_last_activity =>
3511 'circ.longoverdue.use_last_activity_date_on_return',
3512 void_cost_btype => 10,
3513 void_fee_btype => 11
3517 # last billing activity is last payment time, last billing time, or the
3518 # circ due date. If the relevant "use last activity" org unit setting is
3519 # false/unset, then last billing activity is always the due date.
3520 sub get_circ_last_billing_activity {
3522 my $circ_lib = shift;
3523 my $setting = shift;
3524 my $date = $self->circ->due_date;
3526 return $date unless $setting and
3527 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3529 my $xact = $self->editor->retrieve_money_billable_transaction([
3531 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3534 if ($xact->summary) {
3535 $date = $xact->summary->last_payment_ts ||
3536 $xact->summary->last_billing_ts ||
3537 $self->circ->due_date;
3544 sub checkin_handle_lost_or_longoverdue {
3545 my ($self, %args) = @_;
3547 my $circ = $self->circ;
3548 my $max_return = $args{max_return};
3549 my $circ_lib = $args{circ_lib};
3554 $self->get_circ_last_billing_activity(
3555 $circ_lib, $args{ous_use_last_activity});
3558 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3559 $tm[5] -= 1 if $tm[5] > 0;
3560 my $due = timelocal(int($tm[1]), int($tm[2]),
3561 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3564 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3566 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3567 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3568 "DUE: $due LAST: $last_chance");
3570 $max_return = 0 if $today < $last_chance;
3576 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3577 "return interval. skipping fine/fee voiding, etc.");
3579 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3581 $logger->info("circulator: check-in of lost/lo item having a balance ".
3582 "of zero, skipping fine/fee voiding and reinstatement.");
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 $void_cost = $U->ou_ancestor_setting_value(
3591 $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3592 my $void_proc_fee = $U->ou_ancestor_setting_value(
3593 $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3594 my $restore_od = $U->ou_ancestor_setting_value(
3595 $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3597 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3598 if $restore_od && ! $self->void_overdues;
3599 $self->checkin_handle_lost_or_lo_now_found(
3600 $args{void_cost_btype}, $args{is_longoverdue}) if ($void_cost);
3601 $self->checkin_handle_lost_or_lo_now_found(
3602 $args{void_fee_btype}, $args{is_longoverdue}) if ($void_proc_fee);
3605 if ($circ_lib != $self->circ_lib) {
3606 # if the item is not home, check to see if we want to retain the
3607 # lost/longoverdue status at this point in the process
3609 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3610 $args{ous_immediately_available}, $self->editor) || 0;
3612 if ($immediately_available) {
3613 # item status does not need to be retained, so give it a
3614 # reshelving status as if it were a normal checkin
3615 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3618 $logger->info("circulator: leaving lost/longoverdue copy".
3619 " status in place on checkin");
3622 # lost/longoverdue item is home and processed, treat like a normal
3623 # checkin from this point on
3624 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3630 sub checkin_handle_backdate {
3633 # ------------------------------------------------------------------
3634 # clean up the backdate for date comparison
3635 # XXX We are currently taking the due-time from the original due-date,
3636 # not the input. Do we need to do this? This certainly interferes with
3637 # backdating of hourly checkouts, but that is likely a very rare case.
3638 # ------------------------------------------------------------------
3639 my $bd = cleanse_ISO8601($self->backdate);
3640 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3641 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3642 $new_date->set_hour($original_date->hour());
3643 $new_date->set_minute($original_date->minute());
3644 if ($new_date >= DateTime->now) {
3645 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3648 $bd = cleanse_ISO8601($new_date->datetime());
3651 $self->backdate($bd);
3656 sub check_checkin_copy_status {
3658 my $copy = $self->copy;
3660 my $status = $U->copy_status($copy->status)->id;
3663 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3664 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3665 $status == OILS_COPY_STATUS_IN_PROCESS ||
3666 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3667 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3668 $status == OILS_COPY_STATUS_CATALOGING ||
3669 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3670 $status == OILS_COPY_STATUS_RESHELVING );
3672 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3673 if( $status == OILS_COPY_STATUS_LOST );
3675 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3676 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3678 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3679 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3681 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3682 if( $status == OILS_COPY_STATUS_MISSING );
3684 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3689 # --------------------------------------------------------------------------
3690 # On checkin, we need to return as many relevant objects as we can
3691 # --------------------------------------------------------------------------
3692 sub checkin_flesh_events {
3695 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3696 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3697 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3700 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3703 if($self->hold and !$self->hold->cancel_time) {
3704 $hold = $self->hold;
3705 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3709 # update our copy of the circ object and
3710 # flesh the billing summary data
3712 $self->editor->retrieve_action_circulation([
3716 circ => ['billable_transaction'],
3725 # flesh some patron fields before returning
3727 $self->editor->retrieve_actor_user([
3732 au => ['card', 'billing_address', 'mailing_address']
3739 for my $evt (@{$self->events}) {
3742 $payload->{copy} = $U->unflesh_copy($self->copy);
3743 $payload->{volume} = $self->volume;
3744 $payload->{record} = $record,
3745 $payload->{circ} = $self->circ;
3746 $payload->{transit} = $self->transit;
3747 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3748 $payload->{hold} = $hold;
3749 $payload->{patron} = $self->patron;
3750 $payload->{reservation} = $self->reservation
3751 unless (not $self->reservation or $self->reservation->cancel_time);
3753 $evt->{payload} = $payload;
3758 my( $self, $msg ) = @_;
3759 my $bc = ($self->copy) ? $self->copy->barcode :
3762 my $usr = ($self->patron) ? $self->patron->id : "";
3763 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3764 ", recipient=$usr, copy=$bc");
3770 $self->log_me("do_renew()");
3772 # Make sure there is an open circ to renew that is not
3773 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3774 my $usrid = $self->patron->id if $self->patron;
3775 my $circ = $self->editor->search_action_circulation({
3776 target_copy => $self->copy->id,
3777 xact_finish => undef,
3778 checkin_time => undef,
3779 ($usrid ? (usr => $usrid) : ()),
3781 {stop_fines => undef},
3782 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3786 return $self->bail_on_events($self->editor->event) unless $circ;
3788 # A user is not allowed to renew another user's items without permission
3789 unless( $circ->usr eq $self->editor->requestor->id ) {
3790 return $self->bail_on_events($self->editor->events)
3791 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3794 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3795 if $circ->renewal_remaining < 1;
3797 # -----------------------------------------------------------------
3799 $self->parent_circ($circ->id);
3800 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3803 # Opac renewal - re-use circ library from original circ (unless told not to)
3804 if($self->opac_renewal) {
3805 unless(defined($opac_renewal_use_circ_lib)) {
3806 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3807 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3808 $opac_renewal_use_circ_lib = 1;
3811 $opac_renewal_use_circ_lib = 0;
3814 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3817 # Desk renewal - re-use circ library from original circ (unless told not to)
3818 if($self->desk_renewal) {
3819 unless(defined($desk_renewal_use_circ_lib)) {
3820 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3821 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3822 $desk_renewal_use_circ_lib = 1;
3825 $desk_renewal_use_circ_lib = 0;
3828 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3831 # Run the fine generator against the old circ
3832 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3833 # a few lines down. Commenting out, for now.
3834 #$self->handle_fines;
3836 $self->run_renew_permit;
3839 $self->do_checkin();
3840 return if $self->bail_out;
3842 unless( $self->permit_override ) {
3844 return if $self->bail_out;
3845 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3846 $self->remove_event('ITEM_NOT_CATALOGED');
3849 $self->override_events;
3850 return if $self->bail_out;
3853 $self->do_checkout();
3858 my( $self, $evt ) = @_;
3859 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3860 $logger->debug("circulator: removing event from list: $evt");
3861 my @events = @{$self->events};
3862 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3867 my( $self, $evt ) = @_;
3868 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3869 return grep { $_->{textcode} eq $evt } @{$self->events};
3874 sub run_renew_permit {
3877 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3878 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3879 $self->editor, $self->copy, $self->editor->requestor, 1
3881 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3884 if(!$self->legacy_script_support) {
3885 my $results = $self->run_indb_circ_test;
3886 $self->push_events($self->matrix_test_result_events)
3887 unless $self->circ_test_success;
3890 my $runner = $self->script_runner;
3892 $runner->load($self->circ_permit_renew);
3893 my $result = $runner->run or
3894 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3895 if ($result->{"events"}) {
3897 map { new OpenILS::Event($_) } @{$result->{"events"}}
3900 "circulator: circ_permit_renew for user " .
3901 $self->patron->id . " returned " .
3902 scalar(@{$result->{"events"}}) . " event(s)"
3906 $self->mk_script_runner;
3909 $logger->debug("circulator: re-creating script runner to be safe");
3913 # XXX: The primary mechanism for storing circ history is now handled
3914 # by tracking real circulation objects instead of bibs in a bucket.
3915 # However, this code is disabled by default and could be useful
3916 # some day, so may as well leave it for now.
3917 sub append_reading_list {
3921 $self->is_checkout and
3927 # verify history is globally enabled and uses the bucket mechanism
3928 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3929 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3931 return undef unless $htype and $htype eq 'bucket';
3933 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3935 # verify the patron wants to retain the hisory
3936 my $setting = $e->search_actor_user_setting(
3937 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3939 unless($setting and $setting->value) {
3944 my $bkt = $e->search_container_copy_bucket(
3945 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3950 # find the next item position
3951 my $last_item = $e->search_container_copy_bucket_item(
3952 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3953 $pos = $last_item->pos + 1 if $last_item;
3956 # create the history bucket if necessary
3957 $bkt = Fieldmapper::container::copy_bucket->new;
3958 $bkt->owner($self->patron->id);
3960 $bkt->btype('circ_history');
3962 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3965 my $item = Fieldmapper::container::copy_bucket_item->new;
3967 $item->bucket($bkt->id);
3968 $item->target_copy($self->copy->id);
3971 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3978 sub make_trigger_events {
3980 return unless $self->circ;
3981 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3982 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3983 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3988 sub checkin_handle_lost_or_lo_now_found {
3989 my ($self, $bill_type, $is_longoverdue) = @_;
3991 # ------------------------------------------------------------------
3992 # remove charge from patron's account if lost item is returned
3993 # ------------------------------------------------------------------
3995 my $bills = $self->editor->search_money_billing(
3997 xact => $self->circ->id,
4002 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4004 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
4005 for my $bill (@$bills) {
4006 if( !$U->is_true($bill->voided) ) {
4007 $logger->info("$tag item returned - voiding bill ".$bill->id);
4009 $bill->void_time('now');
4010 $bill->voider($self->editor->requestor->id);
4011 my $note = ($bill->note) ? $bill->note . "\n" : '';
4012 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
4014 $self->bail_on_events($self->editor->event)
4015 unless $self->editor->update_money_billing($bill);
4020 sub checkin_handle_lost_or_lo_now_found_restore_od {
4022 my $circ_lib = shift;
4023 my $is_longoverdue = shift;
4024 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4026 # ------------------------------------------------------------------
4027 # restore those overdue charges voided when item was set to lost
4028 # ------------------------------------------------------------------
4030 my $ods = $self->editor->search_money_billing(
4032 xact => $self->circ->id,
4037 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4038 for my $bill (@$ods) {
4039 if( $U->is_true($bill->voided) ) {
4040 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4042 $bill->clear_void_time;
4043 $bill->voider($self->editor->requestor->id);
4044 my $note = ($bill->note) ? $bill->note . "\n" : '';
4045 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4047 $self->bail_on_events($self->editor->event)
4048 unless $self->editor->update_money_billing($bill);