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',
1082 # ---------------------------------------------------------------------
1083 # This pushes any patron-related events into the list but does not
1084 # set bail_out for any events
1085 # ---------------------------------------------------------------------
1086 sub run_patron_permit_scripts {
1088 my $runner = $self->script_runner;
1089 my $patronid = $self->patron->id;
1093 if(!$self->legacy_script_support) {
1095 my $results = $self->run_indb_circ_test;
1096 unless($self->circ_test_success) {
1097 my @trimmed_results;
1099 if ($self->is_noncat) {
1100 # no_item result is OK during noncat checkout
1101 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1105 if ($self->checkout_is_for_hold) {
1106 # if this checkout will fulfill a hold, ignore CIRC blocks
1107 # and rely instead on the (later-checked) FULFILL block
1109 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1110 my $fblock_pens = $self->editor->search_config_standing_penalty(
1111 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1113 for my $res (@$results) {
1114 my $name = $res->{fail_part} || '';
1115 next if grep {$_->name eq $name} @$fblock_pens;
1116 push(@trimmed_results, $res);
1120 # not for hold or noncat
1121 @trimmed_results = @$results;
1125 # update the final set of test results
1126 $self->matrix_test_result(\@trimmed_results);
1128 push @allevents, $self->matrix_test_result_events;
1133 # ---------------------------------------------------------------------
1134 # # Now run the patron permit script
1135 # ---------------------------------------------------------------------
1136 $runner->load($self->circ_permit_patron);
1137 my $result = $runner->run or
1138 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1140 my $patron_events = $result->{events};
1142 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1143 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1144 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1145 $penalties = $penalties->{fatal_penalties};
1147 for my $pen (@$penalties) {
1148 # CIRC blocks are ignored if this is a FULFILL scenario
1149 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1150 my $event = OpenILS::Event->new($pen->name);
1151 $event->{desc} = $pen->label;
1152 push(@allevents, $event);
1155 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1159 $_->{payload} = $self->copy if
1160 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1163 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1165 $self->push_events(@allevents);
1168 sub matrix_test_result_codes {
1170 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1173 sub matrix_test_result_events {
1176 my $event = new OpenILS::Event(
1177 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1179 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1181 } (@{$self->matrix_test_result});
1184 sub run_indb_circ_test {
1186 return $self->matrix_test_result if $self->matrix_test_result;
1188 my $dbfunc = ($self->is_renewal) ?
1189 'action.item_user_renew_test' : 'action.item_user_circ_test';
1191 if( $self->is_precat && $self->request_precat) {
1192 $self->make_precat_copy;
1193 return if $self->bail_out;
1196 my $results = $self->editor->json_query(
1200 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1206 $self->circ_test_success($U->is_true($results->[0]->{success}));
1208 if(my $mp = $results->[0]->{matchpoint}) {
1209 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1210 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1211 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1212 if(defined($results->[0]->{renewals})) {
1213 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1215 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1216 if(defined($results->[0]->{grace_period})) {
1217 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1219 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1220 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1221 # Grab the *last* response for limit_groups, where it is more likely to be filled
1222 $self->limit_groups($results->[-1]->{limit_groups});
1225 return $self->matrix_test_result($results);
1228 # ---------------------------------------------------------------------
1229 # given a use and copy, this will calculate the circulation policy
1230 # parameters. Only works with in-db circ.
1231 # ---------------------------------------------------------------------
1235 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1237 $self->run_indb_circ_test;
1240 circ_test_success => $self->circ_test_success,
1241 failure_events => [],
1242 failure_codes => [],
1243 matchpoint => $self->circ_matrix_matchpoint
1246 unless($self->circ_test_success) {
1247 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1248 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1251 if($self->circ_matrix_matchpoint) {
1252 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1253 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1254 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1255 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1257 my $policy = $self->get_circ_policy(
1258 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1260 $$results{$_} = $$policy{$_} for keys %$policy;
1266 # ---------------------------------------------------------------------
1267 # Loads the circ policy info for duration, recurring fine, and max
1268 # fine based on the current copy
1269 # ---------------------------------------------------------------------
1270 sub get_circ_policy {
1271 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1274 duration_rule => $duration_rule->name,
1275 recurring_fine_rule => $recurring_fine_rule->name,
1276 max_fine_rule => $max_fine_rule->name,
1277 max_fine => $self->get_max_fine_amount($max_fine_rule),
1278 fine_interval => $recurring_fine_rule->recurrence_interval,
1279 renewal_remaining => $duration_rule->max_renewals,
1280 grace_period => $recurring_fine_rule->grace_period
1283 if($hard_due_date) {
1284 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1285 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1288 $policy->{duration_date_ceiling} = undef;
1289 $policy->{duration_date_ceiling_force} = undef;
1292 $policy->{duration} = $duration_rule->shrt
1293 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1294 $policy->{duration} = $duration_rule->normal
1295 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1296 $policy->{duration} = $duration_rule->extended
1297 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1299 $policy->{recurring_fine} = $recurring_fine_rule->low
1300 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1301 $policy->{recurring_fine} = $recurring_fine_rule->normal
1302 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1303 $policy->{recurring_fine} = $recurring_fine_rule->high
1304 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1309 sub get_max_fine_amount {
1311 my $max_fine_rule = shift;
1312 my $max_amount = $max_fine_rule->amount;
1314 # if is_percent is true then the max->amount is
1315 # use as a percentage of the copy price
1316 if ($U->is_true($max_fine_rule->is_percent)) {
1317 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1318 $max_amount = $price * $max_fine_rule->amount / 100;
1320 $U->ou_ancestor_setting_value(
1322 'circ.max_fine.cap_at_price',
1326 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1327 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1335 sub run_copy_permit_scripts {
1337 my $copy = $self->copy || return;
1338 my $runner = $self->script_runner;
1342 if(!$self->legacy_script_support) {
1343 my $results = $self->run_indb_circ_test;
1344 push @allevents, $self->matrix_test_result_events
1345 unless $self->circ_test_success;
1348 # ---------------------------------------------------------------------
1349 # Capture all of the copy permit events
1350 # ---------------------------------------------------------------------
1351 $runner->load($self->circ_permit_copy);
1352 my $result = $runner->run or
1353 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1354 my $copy_events = $result->{events};
1356 # ---------------------------------------------------------------------
1357 # Now collect all of the events together
1358 # ---------------------------------------------------------------------
1359 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1362 # See if this copy has an alert message
1363 my $ae = $self->check_copy_alert();
1364 push( @allevents, $ae ) if $ae;
1366 # uniquify the events
1367 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1368 @allevents = values %hash;
1370 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1372 $self->push_events(@allevents);
1376 sub check_copy_alert {
1378 return undef if $self->is_renewal;
1379 return OpenILS::Event->new(
1380 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1381 if $self->copy and $self->copy->alert_message;
1387 # --------------------------------------------------------------------------
1388 # If the call is overriding and has permissions to override every collected
1389 # event, the are cleared. Any event that the caller does not have
1390 # permission to override, will be left in the event list and bail_out will
1392 # XXX We need code in here to cancel any holds/transits on copies
1393 # that are being force-checked out
1394 # --------------------------------------------------------------------------
1395 sub override_events {
1397 my @events = @{$self->events};
1398 return unless @events;
1399 my $oargs = $self->override_args;
1401 if(!$self->override) {
1402 return $self->bail_out(1)
1403 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1408 for my $e (@events) {
1409 my $tc = $e->{textcode};
1410 next if $tc eq 'SUCCESS';
1411 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1412 my $ov = "$tc.override";
1413 $logger->info("circulator: attempting to override event: $ov");
1415 return $self->bail_on_events($self->editor->event)
1416 unless( $self->editor->allowed($ov) );
1418 return $self->bail_out(1);
1424 # --------------------------------------------------------------------------
1425 # If there is an open claimsreturn circ on the requested copy, close the
1426 # circ if overriding, otherwise bail out
1427 # --------------------------------------------------------------------------
1428 sub handle_claims_returned {
1430 my $copy = $self->copy;
1432 my $CR = $self->editor->search_action_circulation(
1434 target_copy => $copy->id,
1435 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1436 checkin_time => undef,
1440 return unless ($CR = $CR->[0]);
1444 # - If the caller has set the override flag, we will check the item in
1445 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1447 $CR->checkin_time('now');
1448 $CR->checkin_scan_time('now');
1449 $CR->checkin_lib($self->circ_lib);
1450 $CR->checkin_workstation($self->editor->requestor->wsid);
1451 $CR->checkin_staff($self->editor->requestor->id);
1453 $evt = $self->editor->event
1454 unless $self->editor->update_action_circulation($CR);
1457 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1460 $self->bail_on_events($evt) if $evt;
1465 # --------------------------------------------------------------------------
1466 # This performs the checkout
1467 # --------------------------------------------------------------------------
1471 $self->log_me("do_checkout()");
1473 # make sure perms are good if this isn't a renewal
1474 unless( $self->is_renewal ) {
1475 return $self->bail_on_events($self->editor->event)
1476 unless( $self->editor->allowed('COPY_CHECKOUT') );
1479 # verify the permit key
1480 unless( $self->check_permit_key ) {
1481 if( $self->permit_override ) {
1482 return $self->bail_on_events($self->editor->event)
1483 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1485 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1489 # if this is a non-cataloged circ, build the circ and finish
1490 if( $self->is_noncat ) {
1491 $self->checkout_noncat;
1493 OpenILS::Event->new('SUCCESS',
1494 payload => { noncat_circ => $self->circ }));
1498 if( $self->is_precat ) {
1499 $self->make_precat_copy;
1500 return if $self->bail_out;
1502 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1503 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1506 $self->do_copy_checks;
1507 return if $self->bail_out;
1509 $self->run_checkout_scripts();
1510 return if $self->bail_out;
1512 $self->build_checkout_circ_object();
1513 return if $self->bail_out;
1515 my $modify_to_start = $self->booking_adjusted_due_date();
1516 return if $self->bail_out;
1518 $self->apply_modified_due_date($modify_to_start);
1519 return if $self->bail_out;
1521 return $self->bail_on_events($self->editor->event)
1522 unless $self->editor->create_action_circulation($self->circ);
1524 # refresh the circ to force local time zone for now
1525 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1527 if($self->limit_groups) {
1528 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1531 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1533 return if $self->bail_out;
1535 $self->apply_deposit_fee();
1536 return if $self->bail_out;
1538 $self->handle_checkout_holds();
1539 return if $self->bail_out;
1541 # ------------------------------------------------------------------------------
1542 # Update the patron penalty info in the DB. Run it for permit-overrides
1543 # since the penalties are not updated during the permit phase
1544 # ------------------------------------------------------------------------------
1545 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1547 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1550 if($self->is_renewal) {
1551 # flesh the billing summary for the checked-in circ
1552 $pcirc = $self->editor->retrieve_action_circulation([
1554 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1559 OpenILS::Event->new('SUCCESS',
1561 copy => $U->unflesh_copy($self->copy),
1562 volume => $self->volume,
1563 circ => $self->circ,
1565 holds_fulfilled => $self->fulfilled_holds,
1566 deposit_billing => $self->deposit_billing,
1567 rental_billing => $self->rental_billing,
1568 parent_circ => $pcirc,
1569 patron => ($self->return_patron) ? $self->patron : undef,
1570 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1576 sub apply_deposit_fee {
1578 my $copy = $self->copy;
1580 ($self->is_deposit and not $self->is_deposit_exempt) or
1581 ($self->is_rental and not $self->is_rental_exempt);
1583 return if $self->is_deposit and $self->skip_deposit_fee;
1584 return if $self->is_rental and $self->skip_rental_fee;
1586 my $bill = Fieldmapper::money::billing->new;
1587 my $amount = $copy->deposit_amount;
1591 if($self->is_deposit) {
1592 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1594 $self->deposit_billing($bill);
1596 $billing_type = OILS_BILLING_TYPE_RENTAL;
1598 $self->rental_billing($bill);
1601 $bill->xact($self->circ->id);
1602 $bill->amount($amount);
1603 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1604 $bill->billing_type($billing_type);
1605 $bill->btype($btype);
1606 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1608 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1613 my $copy = $self->copy;
1615 my $stat = $copy->status if ref $copy->status;
1616 my $loc = $copy->location if ref $copy->location;
1617 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1619 $copy->status($stat->id) if $stat;
1620 $copy->location($loc->id) if $loc;
1621 $copy->circ_lib($circ_lib->id) if $circ_lib;
1622 $copy->editor($self->editor->requestor->id);
1623 $copy->edit_date('now');
1624 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1626 return $self->bail_on_events($self->editor->event)
1627 unless $self->editor->update_asset_copy($self->copy);
1629 $copy->status($U->copy_status($copy->status));
1630 $copy->location($loc) if $loc;
1631 $copy->circ_lib($circ_lib) if $circ_lib;
1634 sub update_reservation {
1636 my $reservation = $self->reservation;
1638 my $usr = $reservation->usr;
1639 my $target_rt = $reservation->target_resource_type;
1640 my $target_r = $reservation->target_resource;
1641 my $current_r = $reservation->current_resource;
1643 $reservation->usr($usr->id) if ref $usr;
1644 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1645 $reservation->target_resource($target_r->id) if ref $target_r;
1646 $reservation->current_resource($current_r->id) if ref $current_r;
1648 return $self->bail_on_events($self->editor->event)
1649 unless $self->editor->update_booking_reservation($self->reservation);
1652 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1653 $self->reservation($reservation);
1657 sub bail_on_events {
1658 my( $self, @evts ) = @_;
1659 $self->push_events(@evts);
1663 # ------------------------------------------------------------------------------
1664 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1665 # affects copies that will fulfill holds and CIRC affects all other copies.
1666 # If blocks exists, bail, push Events onto the event pile, and return true.
1667 # ------------------------------------------------------------------------------
1668 sub check_hold_fulfill_blocks {
1671 # See if the user has any penalties applied that prevent hold fulfillment
1672 my $pens = $self->editor->json_query({
1673 select => {csp => ['name', 'label']},
1674 from => {ausp => {csp => {}}},
1677 usr => $self->patron->id,
1678 org_unit => $U->get_org_full_path($self->circ_lib),
1680 {stop_date => undef},
1681 {stop_date => {'>' => 'now'}}
1684 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1688 return 0 unless @$pens;
1690 for my $pen (@$pens) {
1691 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1692 my $event = OpenILS::Event->new($pen->{name});
1693 $event->{desc} = $pen->{label};
1694 $self->push_events($event);
1697 $self->override_events;
1698 return $self->bail_out;
1702 # ------------------------------------------------------------------------------
1703 # When an item is checked out, see if we can fulfill a hold for this patron
1704 # ------------------------------------------------------------------------------
1705 sub handle_checkout_holds {
1707 my $copy = $self->copy;
1708 my $patron = $self->patron;
1710 my $e = $self->editor;
1711 $self->fulfilled_holds([]);
1713 # non-cats can't fulfill a hold
1714 return if $self->is_noncat;
1716 my $hold = $e->search_action_hold_request({
1717 current_copy => $copy->id ,
1718 cancel_time => undef,
1719 fulfillment_time => undef,
1721 {expire_time => undef},
1722 {expire_time => {'>' => 'now'}}
1726 if($hold and $hold->usr != $patron->id) {
1727 # reset the hold since the copy is now checked out
1729 $logger->info("circulator: un-targeting hold ".$hold->id.
1730 " because copy ".$copy->id." is getting checked out");
1732 $hold->clear_prev_check_time;
1733 $hold->clear_current_copy;
1734 $hold->clear_capture_time;
1735 $hold->clear_shelf_time;
1736 $hold->clear_shelf_expire_time;
1737 $hold->clear_current_shelf_lib;
1739 return $self->bail_on_event($e->event)
1740 unless $e->update_action_hold_request($hold);
1746 $hold = $self->find_related_user_hold($copy, $patron) or return;
1747 $logger->info("circulator: found related hold to fulfill in checkout");
1750 return if $self->check_hold_fulfill_blocks;
1752 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1754 # if the hold was never officially captured, capture it.
1755 $hold->current_copy($copy->id);
1756 $hold->capture_time('now') unless $hold->capture_time;
1757 $hold->fulfillment_time('now');
1758 $hold->fulfillment_staff($e->requestor->id);
1759 $hold->fulfillment_lib($self->circ_lib);
1761 return $self->bail_on_events($e->event)
1762 unless $e->update_action_hold_request($hold);
1764 return $self->fulfilled_holds([$hold->id]);
1768 # ------------------------------------------------------------------------------
1769 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1770 # the patron directly targets the checked out item, see if there is another hold
1771 # for the patron that could be fulfilled by the checked out item. Fulfill the
1772 # oldest hold and only fulfill 1 of them.
1774 # For "another hold":
1776 # First, check for one that the copy matches via hold_copy_map, ensuring that
1777 # *any* hold type that this copy could fill may end up filled.
1779 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1780 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1781 # that are non-requestable to count as capturing those hold types.
1782 # ------------------------------------------------------------------------------
1783 sub find_related_user_hold {
1784 my($self, $copy, $patron) = @_;
1785 my $e = $self->editor;
1787 # holds on precat copies are always copy-level, so this call will
1788 # always return undef. Exit early.
1789 return undef if $self->is_precat;
1791 return undef unless $U->ou_ancestor_setting_value(
1792 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1794 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1796 select => {ahr => ['id']},
1805 fkey => 'current_copy',
1806 type => 'left' # there may be no current_copy
1813 fulfillment_time => undef,
1814 cancel_time => undef,
1816 {expire_time => undef},
1817 {expire_time => {'>' => 'now'}}
1821 target_copy => $self->copy->id
1825 {id => undef}, # left-join copy may be nonexistent
1826 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1830 order_by => {ahr => {request_time => {direction => 'asc'}}},
1834 my $hold_info = $e->json_query($args)->[0];
1835 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1836 return undef if $U->ou_ancestor_setting_value(
1837 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1839 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1841 select => {ahr => ['id']},
1846 fkey => 'current_copy',
1847 type => 'left' # there may be no current_copy
1854 fulfillment_time => undef,
1855 cancel_time => undef,
1857 {expire_time => undef},
1858 {expire_time => {'>' => 'now'}}
1865 target => $self->volume->id
1871 target => $self->title->id
1877 {id => undef}, # left-join copy may be nonexistent
1878 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1882 order_by => {ahr => {request_time => {direction => 'asc'}}},
1886 $hold_info = $e->json_query($args)->[0];
1887 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1892 sub run_checkout_scripts {
1897 my $runner = $self->script_runner;
1906 my $hard_due_date_name;
1908 if(!$self->legacy_script_support) {
1909 $self->run_indb_circ_test();
1910 $duration = $self->circ_matrix_matchpoint->duration_rule;
1911 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1912 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1913 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1917 $runner->load($self->circ_duration);
1919 my $result = $runner->run or
1920 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1922 $duration_name = $result->{durationRule};
1923 $recurring_name = $result->{recurringFinesRule};
1924 $max_fine_name = $result->{maxFine};
1925 $hard_due_date_name = $result->{hardDueDate};
1928 $duration_name = $duration->name if $duration;
1929 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1932 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1933 return $self->bail_on_events($evt) if ($evt && !$nobail);
1935 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1936 return $self->bail_on_events($evt) if ($evt && !$nobail);
1938 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1939 return $self->bail_on_events($evt) if ($evt && !$nobail);
1941 if($hard_due_date_name) {
1942 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1943 return $self->bail_on_events($evt) if ($evt && !$nobail);
1949 # The item circulates with an unlimited duration
1953 $hard_due_date = undef;
1956 $self->duration_rule($duration);
1957 $self->recurring_fines_rule($recurring);
1958 $self->max_fine_rule($max_fine);
1959 $self->hard_due_date($hard_due_date);
1963 sub build_checkout_circ_object {
1966 my $circ = Fieldmapper::action::circulation->new;
1967 my $duration = $self->duration_rule;
1968 my $max = $self->max_fine_rule;
1969 my $recurring = $self->recurring_fines_rule;
1970 my $hard_due_date = $self->hard_due_date;
1971 my $copy = $self->copy;
1972 my $patron = $self->patron;
1973 my $duration_date_ceiling;
1974 my $duration_date_ceiling_force;
1978 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1979 $duration_date_ceiling = $policy->{duration_date_ceiling};
1980 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1982 my $dname = $duration->name;
1983 my $mname = $max->name;
1984 my $rname = $recurring->name;
1986 if($hard_due_date) {
1987 $hdname = $hard_due_date->name;
1990 $logger->debug("circulator: building circulation ".
1991 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1993 $circ->duration($policy->{duration});
1994 $circ->recurring_fine($policy->{recurring_fine});
1995 $circ->duration_rule($duration->name);
1996 $circ->recurring_fine_rule($recurring->name);
1997 $circ->max_fine_rule($max->name);
1998 $circ->max_fine($policy->{max_fine});
1999 $circ->fine_interval($recurring->recurrence_interval);
2000 $circ->renewal_remaining($duration->max_renewals);
2001 $circ->grace_period($policy->{grace_period});
2005 $logger->info("circulator: copy found with an unlimited circ duration");
2006 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2007 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2008 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2009 $circ->renewal_remaining(0);
2010 $circ->grace_period(0);
2013 $circ->target_copy( $copy->id );
2014 $circ->usr( $patron->id );
2015 $circ->circ_lib( $self->circ_lib );
2016 $circ->workstation($self->editor->requestor->wsid)
2017 if defined $self->editor->requestor->wsid;
2019 # renewals maintain a link to the parent circulation
2020 $circ->parent_circ($self->parent_circ);
2022 if( $self->is_renewal ) {
2023 $circ->opac_renewal('t') if $self->opac_renewal;
2024 $circ->phone_renewal('t') if $self->phone_renewal;
2025 $circ->desk_renewal('t') if $self->desk_renewal;
2026 $circ->renewal_remaining($self->renewal_remaining);
2027 $circ->circ_staff($self->editor->requestor->id);
2031 # if the user provided an overiding checkout time,
2032 # (e.g. the checkout really happened several hours ago), then
2033 # we apply that here. Does this need a perm??
2034 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2035 if $self->checkout_time;
2037 # if a patron is renewing, 'requestor' will be the patron
2038 $circ->circ_staff($self->editor->requestor->id);
2039 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2044 sub do_reservation_pickup {
2047 $self->log_me("do_reservation_pickup()");
2049 $self->reservation->pickup_time('now');
2052 $self->reservation->current_resource &&
2053 $U->is_true($self->reservation->target_resource_type->catalog_item)
2055 # We used to try to set $self->copy and $self->patron here,
2056 # but that should already be done.
2058 $self->run_checkout_scripts(1);
2060 my $duration = $self->duration_rule;
2061 my $max = $self->max_fine_rule;
2062 my $recurring = $self->recurring_fines_rule;
2064 if ($duration && $max && $recurring) {
2065 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2067 my $dname = $duration->name;
2068 my $mname = $max->name;
2069 my $rname = $recurring->name;
2071 $logger->debug("circulator: updating reservation ".
2072 "with duration=$dname, maxfine=$mname, recurring=$rname");
2074 $self->reservation->fine_amount($policy->{recurring_fine});
2075 $self->reservation->max_fine($policy->{max_fine});
2076 $self->reservation->fine_interval($recurring->recurrence_interval);
2079 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2080 $self->update_copy();
2083 $self->reservation->fine_amount(
2084 $self->reservation->target_resource_type->fine_amount
2086 $self->reservation->max_fine(
2087 $self->reservation->target_resource_type->max_fine
2089 $self->reservation->fine_interval(
2090 $self->reservation->target_resource_type->fine_interval
2094 $self->update_reservation();
2097 sub do_reservation_return {
2099 my $request = shift;
2101 $self->log_me("do_reservation_return()");
2103 if (not ref $self->reservation) {
2104 my ($reservation, $evt) =
2105 $U->fetch_booking_reservation($self->reservation);
2106 return $self->bail_on_events($evt) if $evt;
2107 $self->reservation($reservation);
2110 $self->handle_fines(1);
2111 $self->reservation->return_time('now');
2112 $self->update_reservation();
2113 $self->reshelve_copy if $self->copy;
2115 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2116 $self->copy( $self->reservation->current_resource->catalog_item );
2120 sub booking_adjusted_due_date {
2122 my $circ = $self->circ;
2123 my $copy = $self->copy;
2125 return undef unless $self->use_booking;
2129 if( $self->due_date ) {
2131 return $self->bail_on_events($self->editor->event)
2132 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2134 $circ->due_date(cleanse_ISO8601($self->due_date));
2138 return unless $copy and $circ->due_date;
2141 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2142 if (@$booking_items) {
2143 my $booking_item = $booking_items->[0];
2144 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2146 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2147 my $shorten_circ_setting = $resource_type->elbow_room ||
2148 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2151 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2152 my $bookings = $booking_ses->request(
2153 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2154 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2156 $booking_ses->disconnect;
2158 my $dt_parser = DateTime::Format::ISO8601->new;
2159 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2161 for my $bid (@$bookings) {
2163 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2165 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2166 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2168 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2169 if ($booking_start < DateTime->now);
2172 if ($U->is_true($stop_circ_setting)) {
2173 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2175 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2176 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2179 # We set the circ duration here only to affect the logic that will
2180 # later (in a DB trigger) mangle the time part of the due date to
2181 # 11:59pm. Having any circ duration that is not a whole number of
2182 # days is enough to prevent the "correction."
2183 my $new_circ_duration = $due_date->epoch - time;
2184 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2185 $circ->duration("$new_circ_duration seconds");
2187 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2191 return $self->bail_on_events($self->editor->event)
2192 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2198 sub apply_modified_due_date {
2200 my $shift_earlier = shift;
2201 my $circ = $self->circ;
2202 my $copy = $self->copy;
2204 if( $self->due_date ) {
2206 return $self->bail_on_events($self->editor->event)
2207 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2209 $circ->due_date(cleanse_ISO8601($self->due_date));
2213 # if the due_date lands on a day when the location is closed
2214 return unless $copy and $circ->due_date;
2216 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2218 # due-date overlap should be determined by the location the item
2219 # is checked out from, not the owning or circ lib of the item
2220 my $org = $self->circ_lib;
2222 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2223 " with an item due date of ".$circ->due_date );
2225 my $dateinfo = $U->storagereq(
2226 'open-ils.storage.actor.org_unit.closed_date.overlap',
2227 $org, $circ->due_date );
2230 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2231 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2233 # XXX make the behavior more dynamic
2234 # for now, we just push the due date to after the close date
2235 if ($shift_earlier) {
2236 $circ->due_date($dateinfo->{start});
2238 $circ->due_date($dateinfo->{end});
2246 sub create_due_date {
2247 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2249 # if there is a raw time component (e.g. from postgres),
2250 # turn it into an interval that interval_to_seconds can parse
2251 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2253 # for now, use the server timezone. TODO: use workstation org timezone
2254 my $due_date = DateTime->now(time_zone => 'local');
2255 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2257 # add the circ duration
2258 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2261 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2262 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2263 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2268 # return ISO8601 time with timezone
2269 return $due_date->strftime('%FT%T%z');
2274 sub make_precat_copy {
2276 my $copy = $self->copy;
2279 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2281 $copy->editor($self->editor->requestor->id);
2282 $copy->edit_date('now');
2283 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2284 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2285 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2286 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2287 $self->update_copy();
2291 $logger->info("circulator: Creating a new precataloged ".
2292 "copy in checkout with barcode " . $self->copy_barcode);
2294 $copy = Fieldmapper::asset::copy->new;
2295 $copy->circ_lib($self->circ_lib);
2296 $copy->creator($self->editor->requestor->id);
2297 $copy->editor($self->editor->requestor->id);
2298 $copy->barcode($self->copy_barcode);
2299 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2300 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2301 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2303 $copy->dummy_title($self->dummy_title || "");
2304 $copy->dummy_author($self->dummy_author || "");
2305 $copy->dummy_isbn($self->dummy_isbn || "");
2306 $copy->circ_modifier($self->circ_modifier);
2309 # See if we need to override the circ_lib for the copy with a configured circ_lib
2310 # Setting is shortname of the org unit
2311 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2312 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2314 if($precat_circ_lib) {
2315 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2318 $self->bail_on_events($self->editor->event);
2322 $copy->circ_lib($org->id);
2326 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2328 $self->push_events($self->editor->event);
2332 # this is a little bit of a hack, but we need to
2333 # get the copy into the script runner
2334 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2338 sub checkout_noncat {
2344 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2345 my $count = $self->noncat_count || 1;
2346 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2348 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2352 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2353 $self->editor->requestor->id,
2361 $self->push_events($evt);
2369 # If a copy goes into transit and is then checked in before the transit checkin
2370 # interval has expired, push an event onto the overridable events list.
2371 sub check_transit_checkin_interval {
2374 # only concerned with in-transit items
2375 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2377 # no interval, no problem
2378 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2379 return unless $interval;
2381 # capture the transit so we don't have to fetch it again later during checkin
2383 $self->editor->search_action_transit_copy(
2384 {target_copy => $self->copy->id, dest_recv_time => undef}
2388 # transit from X to X for whatever reason has no min interval
2389 return if $self->transit->source == $self->transit->dest;
2391 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2392 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2393 my $horizon = $t_start->add(seconds => $seconds);
2395 # See if we are still within the transit checkin forbidden range
2396 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2397 if $horizon > DateTime->now;
2400 # Retarget local holds at checkin
2401 sub checkin_retarget {
2403 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2404 return unless $self->is_checkin; # Renewals need not be checked
2405 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2406 return if $self->is_precat; # No holds for precats
2407 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2408 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2409 my $status = $U->copy_status($self->copy->status);
2410 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2411 # Specifically target items that are likely new (by status ID)
2412 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2413 my $location = $self->copy->location;
2414 if(!ref($location)) {
2415 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2416 $self->copy->location($location);
2418 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2420 # Fetch holds for the bib
2421 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2422 $self->editor->authtoken,
2425 capture_time => undef, # No touching captured holds
2426 frozen => 'f', # Don't bother with frozen holds
2427 pickup_lib => $self->circ_lib # Only holds actually here
2430 # Error? Skip the step.
2431 return if exists $result->{"ilsevent"};
2435 foreach my $holdlist (keys %{$result}) {
2436 push @$holds, @{$result->{$holdlist}};
2439 return if scalar(@$holds) == 0; # No holds, no retargeting
2441 # Check for parts on this copy
2442 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2443 my %parts_hash = ();
2444 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2446 # Loop over holds in request-ish order
2447 # Stage 1: Get them into request-ish order
2448 # Also grab type and target for skipping low hanging ones
2449 $result = $self->editor->json_query({
2450 "select" => { "ahr" => ["id", "hold_type", "target"] },
2451 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2452 "where" => { "id" => $holds },
2454 { "class" => "pgt", "field" => "hold_priority"},
2455 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2456 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2457 { "class" => "ahr", "field" => "request_time"}
2462 if (ref $result eq "ARRAY" and scalar @$result) {
2463 foreach (@{$result}) {
2464 # Copy level, but not this copy?
2465 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2466 and $_->{target} != $self->copy->id);
2467 # Volume level, but not this volume?
2468 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2469 if(@$parts) { # We have parts?
2471 next if ($_->{hold_type} eq 'T');
2472 # Skip part holds for parts not on this copy
2473 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2475 # No parts, no part holds
2476 next if ($_->{hold_type} eq 'P');
2478 # So much for easy stuff, attempt a retarget!
2479 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2480 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2481 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2489 $self->log_me("do_checkin()");
2491 return $self->bail_on_events(
2492 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2495 $self->check_transit_checkin_interval;
2496 $self->checkin_retarget;
2498 # the renew code and mk_env should have already found our circulation object
2499 unless( $self->circ ) {
2501 my $circs = $self->editor->search_action_circulation(
2502 { target_copy => $self->copy->id, checkin_time => undef });
2504 $self->circ($$circs[0]);
2506 # for now, just warn if there are multiple open circs on a copy
2507 $logger->warn("circulator: we have ".scalar(@$circs).
2508 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2511 my $stat = $U->copy_status($self->copy->status)->id;
2513 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2514 # differently if they are already paid for. We need to check for this
2515 # early since overdue generation is potentially affected.
2516 my $dont_change_lost_zero = 0;
2517 if ($stat == OILS_COPY_STATUS_LOST
2518 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2519 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2521 # LOST fine settings are controlled by the copy's circ lib, not the the
2523 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2524 $self->copy->circ_lib->id : $self->copy->circ_lib;
2525 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2526 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2527 $self->editor) || 0;
2529 if ($dont_change_lost_zero) {
2530 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2531 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2534 $self->dont_change_lost_zero($dont_change_lost_zero);
2537 my $ignore_stop_fines = undef;
2538 if ($self->circ and !$dont_change_lost_zero) {
2540 # if this circ is LOST and we are configured to generate overdue
2541 # fines for lost items on checkin (to fill the gap between mark
2542 # lost time and when the fines would have naturally stopped), tell
2543 # the fine generator to ignore the stop-fines value on this circ.
2544 # XXX should this setting come from the copy circ lib (like other
2545 # LOST settings), instead of the circulation circ lib?
2546 if ($stat == OILS_COPY_STATUS_LOST) {
2547 $ignore_stop_fines = $self->circ->stop_fines if
2548 $U->ou_ancestor_setting_value(
2550 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2555 # run the fine generator against this circ
2556 $self->handle_fines(undef, $ignore_stop_fines);
2559 if( $self->checkin_check_holds_shelf() ) {
2560 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2561 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2562 if($self->fake_hold_dest) {
2563 $self->hold->pickup_lib($self->circ_lib);
2565 $self->checkin_flesh_events;
2569 unless( $self->is_renewal ) {
2570 return $self->bail_on_events($self->editor->event)
2571 unless $self->editor->allowed('COPY_CHECKIN');
2574 $self->push_events($self->check_copy_alert());
2575 $self->push_events($self->check_checkin_copy_status());
2577 # if the circ is marked as 'claims returned', add the event to the list
2578 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2579 if ($self->circ and $self->circ->stop_fines
2580 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2582 $self->check_circ_deposit();
2584 # handle the overridable events
2585 $self->override_events unless $self->is_renewal;
2586 return if $self->bail_out;
2588 if( $self->copy and !$self->transit ) {
2590 $self->editor->search_action_transit_copy(
2591 { target_copy => $self->copy->id, dest_recv_time => undef }
2597 $self->checkin_handle_circ;
2598 return if $self->bail_out;
2599 $self->checkin_changed(1);
2601 } elsif( $self->transit ) {
2602 my $hold_transit = $self->process_received_transit;
2603 $self->checkin_changed(1);
2605 if( $self->bail_out ) {
2606 $self->checkin_flesh_events;
2610 if( my $e = $self->check_checkin_copy_status() ) {
2611 # If the original copy status is special, alert the caller
2612 my $ev = $self->events;
2613 $self->events([$e]);
2614 $self->override_events;
2615 return if $self->bail_out;
2619 if( $hold_transit or
2620 $U->copy_status($self->copy->status)->id
2621 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2624 if( $hold_transit ) {
2625 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2627 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2632 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2634 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2635 $self->reshelve_copy(1);
2636 $self->cancelled_hold_transit(1);
2637 $self->notify_hold(0); # don't notify for cancelled holds
2638 $self->fake_hold_dest(0);
2639 return if $self->bail_out;
2641 } elsif ($hold and $hold->hold_type eq 'R') {
2643 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2644 $self->notify_hold(0); # No need to notify
2645 $self->fake_hold_dest(0);
2646 $self->noop(1); # Don't try and capture for other holds/transits now
2647 $self->update_copy();
2648 $hold->fulfillment_time('now');
2649 $self->bail_on_events($self->editor->event)
2650 unless $self->editor->update_action_hold_request($hold);
2654 # hold transited to correct location
2655 if($self->fake_hold_dest) {
2656 $hold->pickup_lib($self->circ_lib);
2658 $self->checkin_flesh_events;
2663 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2665 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2666 " that is in-transit, but there is no transit.. repairing");
2667 $self->reshelve_copy(1);
2668 return if $self->bail_out;
2671 if( $self->is_renewal ) {
2672 $self->finish_fines_and_voiding;
2673 return if $self->bail_out;
2674 $self->push_events(OpenILS::Event->new('SUCCESS'));
2678 # ------------------------------------------------------------------------------
2679 # Circulations and transits are now closed where necessary. Now go on to see if
2680 # this copy can fulfill a hold or needs to be routed to a different location
2681 # ------------------------------------------------------------------------------
2683 my $needed_for_something = 0; # formerly "needed_for_hold"
2685 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2687 if (!$self->remote_hold) {
2688 if ($self->use_booking) {
2689 my $potential_hold = $self->hold_capture_is_possible;
2690 my $potential_reservation = $self->reservation_capture_is_possible;
2692 if ($potential_hold and $potential_reservation) {
2693 $logger->info("circulator: item could fulfill either hold or reservation");
2694 $self->push_events(new OpenILS::Event(
2695 "HOLD_RESERVATION_CONFLICT",
2696 "hold" => $potential_hold,
2697 "reservation" => $potential_reservation
2699 return if $self->bail_out;
2700 } elsif ($potential_hold) {
2701 $needed_for_something =
2702 $self->attempt_checkin_hold_capture;
2703 } elsif ($potential_reservation) {
2704 $needed_for_something =
2705 $self->attempt_checkin_reservation_capture;
2708 $needed_for_something = $self->attempt_checkin_hold_capture;
2711 return if $self->bail_out;
2713 unless($needed_for_something) {
2714 my $circ_lib = (ref $self->copy->circ_lib) ?
2715 $self->copy->circ_lib->id : $self->copy->circ_lib;
2717 if( $self->remote_hold ) {
2718 $circ_lib = $self->remote_hold->pickup_lib;
2719 $logger->warn("circulator: Copy ".$self->copy->barcode.
2720 " is on a remote hold's shelf, sending to $circ_lib");
2723 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2725 my $suppress_transit = 0;
2727 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2728 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2729 if($suppress_transit_source && $suppress_transit_source->{value}) {
2730 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2731 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2732 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2733 $suppress_transit = 1;
2738 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2739 # copy is where it needs to be, either for hold or reshelving
2741 $self->checkin_handle_precat();
2742 return if $self->bail_out;
2745 # copy needs to transit "home", or stick here if it's a floating copy
2747 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2748 my $res = $self->editor->json_query(
2750 'evergreen.can_float',
2751 $self->copy->floating->id,
2752 $self->copy->circ_lib,
2757 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2759 if ($can_float) { # Yep, floating, stick here
2760 $self->checkin_changed(1);
2761 $self->copy->circ_lib( $self->circ_lib );
2764 my $bc = $self->copy->barcode;
2765 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2766 $self->checkin_build_copy_transit($circ_lib);
2767 return if $self->bail_out;
2768 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2772 } else { # no-op checkin
2773 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2774 $self->checkin_changed(1);
2775 $self->copy->circ_lib( $self->circ_lib );
2780 if($self->claims_never_checked_out and
2781 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2783 # the item was not supposed to be checked out to the user and should now be marked as missing
2784 $self->copy->status(OILS_COPY_STATUS_MISSING);
2788 $self->reshelve_copy unless $needed_for_something;
2791 return if $self->bail_out;
2793 unless($self->checkin_changed) {
2795 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2796 my $stat = $U->copy_status($self->copy->status)->id;
2798 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2799 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2800 $self->bail_out(1); # no need to commit anything
2804 $self->push_events(OpenILS::Event->new('SUCCESS'))
2805 unless @{$self->events};
2808 $self->finish_fines_and_voiding;
2810 OpenILS::Utils::Penalty->calculate_penalties(
2811 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2813 $self->checkin_flesh_events;
2817 sub finish_fines_and_voiding {
2819 return unless $self->circ;
2821 return unless $self->backdate or $self->void_overdues;
2823 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2824 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2826 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2827 $self->editor, $self->circ, $self->backdate, $note);
2829 return $self->bail_on_events($evt) if $evt;
2831 # Make sure the circ is open or closed as necessary.
2832 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2833 return $self->bail_on_events($evt) if $evt;
2839 # if a deposit was payed for this item, push the event
2840 sub check_circ_deposit {
2842 return unless $self->circ;
2843 my $deposit = $self->editor->search_money_billing(
2845 xact => $self->circ->id,
2847 }, {idlist => 1})->[0];
2849 $self->push_events(OpenILS::Event->new(
2850 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2855 my $force = $self->force || shift;
2856 my $copy = $self->copy;
2858 my $stat = $U->copy_status($copy->status)->id;
2861 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2862 $stat != OILS_COPY_STATUS_CATALOGING and
2863 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2864 $stat != OILS_COPY_STATUS_RESHELVING )) {
2866 $copy->status( OILS_COPY_STATUS_RESHELVING );
2868 $self->checkin_changed(1);
2873 # Returns true if the item is at the current location
2874 # because it was transited there for a hold and the
2875 # hold has not been fulfilled
2876 sub checkin_check_holds_shelf {
2878 return 0 unless $self->copy;
2881 $U->copy_status($self->copy->status)->id ==
2882 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2884 # Attempt to clear shelf expired holds for this copy
2885 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2886 if($self->clear_expired);
2888 # find the hold that put us on the holds shelf
2889 my $holds = $self->editor->search_action_hold_request(
2891 current_copy => $self->copy->id,
2892 capture_time => { '!=' => undef },
2893 fulfillment_time => undef,
2894 cancel_time => undef,
2899 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2900 $self->reshelve_copy(1);
2904 my $hold = $$holds[0];
2906 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2907 $hold->id. "] for copy ".$self->copy->barcode);
2909 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2910 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2911 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2912 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2913 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2914 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2915 $self->fake_hold_dest(1);
2921 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2922 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2926 $logger->info("circulator: hold is not for here..");
2927 $self->remote_hold($hold);
2932 sub checkin_handle_precat {
2934 my $copy = $self->copy;
2936 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2937 $copy->status(OILS_COPY_STATUS_CATALOGING);
2938 $self->update_copy();
2939 $self->checkin_changed(1);
2940 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2945 sub checkin_build_copy_transit {
2948 my $copy = $self->copy;
2949 my $transit = Fieldmapper::action::transit_copy->new;
2951 # if we are transiting an item to the shelf shelf, it's a hold transit
2952 if (my $hold = $self->remote_hold) {
2953 $transit = Fieldmapper::action::hold_transit_copy->new;
2954 $transit->hold($hold->id);
2956 # the item is going into transit, remove any shelf-iness
2957 if ($hold->current_shelf_lib or $hold->shelf_time) {
2958 $hold->clear_current_shelf_lib;
2959 $hold->clear_shelf_time;
2960 return $self->bail_on_events($self->editor->event)
2961 unless $self->editor->update_action_hold_request($hold);
2965 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2966 $logger->info("circulator: transiting copy to $dest");
2968 $transit->source($self->circ_lib);
2969 $transit->dest($dest);
2970 $transit->target_copy($copy->id);
2971 $transit->source_send_time('now');
2972 $transit->copy_status( $U->copy_status($copy->status)->id );
2974 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2976 if ($self->remote_hold) {
2977 return $self->bail_on_events($self->editor->event)
2978 unless $self->editor->create_action_hold_transit_copy($transit);
2980 return $self->bail_on_events($self->editor->event)
2981 unless $self->editor->create_action_transit_copy($transit);
2984 # ensure the transit is returned to the caller
2985 $self->transit($transit);
2987 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2989 $self->checkin_changed(1);
2993 sub hold_capture_is_possible {
2995 my $copy = $self->copy;
2997 # we've been explicitly told not to capture any holds
2998 return 0 if $self->capture eq 'nocapture';
3000 # See if this copy can fulfill any holds
3001 my $hold = $holdcode->find_nearest_permitted_hold(
3002 $self->editor, $copy, $self->editor->requestor, 1 # check_only
3004 return undef if ref $hold eq "HASH" and
3005 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
3009 sub reservation_capture_is_possible {
3011 my $copy = $self->copy;
3013 # we've been explicitly told not to capture any holds
3014 return 0 if $self->capture eq 'nocapture';
3016 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3017 my $resv = $booking_ses->request(
3018 "open-ils.booking.reservations.could_capture",
3019 $self->editor->authtoken, $copy->barcode
3021 $booking_ses->disconnect;
3022 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
3023 $self->push_events($resv);
3029 # returns true if the item was used (or may potentially be used
3030 # in subsequent calls) to capture a hold.
3031 sub attempt_checkin_hold_capture {
3033 my $copy = $self->copy;
3035 # we've been explicitly told not to capture any holds
3036 return 0 if $self->capture eq 'nocapture';
3038 # See if this copy can fulfill any holds
3039 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3040 $self->editor, $copy, $self->editor->requestor );
3043 $logger->debug("circulator: no potential permitted".
3044 "holds found for copy ".$copy->barcode);
3048 if($self->capture ne 'capture') {
3049 # see if this item is in a hold-capture-delay location
3050 my $location = $self->copy->location;
3051 if(!ref($location)) {
3052 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3053 $self->copy->location($location);
3055 if($U->is_true($location->hold_verify)) {
3056 $self->bail_on_events(
3057 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3062 $self->retarget($retarget);
3064 my $suppress_transit = 0;
3065 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3066 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3067 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3068 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3069 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3070 $suppress_transit = 1;
3071 $hold->pickup_lib($self->circ_lib);
3076 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3078 $hold->current_copy($copy->id);
3079 $hold->capture_time('now');
3080 $self->put_hold_on_shelf($hold)
3081 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3083 # prevent DB errors caused by fetching
3084 # holds from storage, and updating through cstore
3085 $hold->clear_fulfillment_time;
3086 $hold->clear_fulfillment_staff;
3087 $hold->clear_fulfillment_lib;
3088 $hold->clear_expire_time;
3089 $hold->clear_cancel_time;
3090 $hold->clear_prev_check_time unless $hold->prev_check_time;
3092 $self->bail_on_events($self->editor->event)
3093 unless $self->editor->update_action_hold_request($hold);
3095 $self->checkin_changed(1);
3097 return 0 if $self->bail_out;
3099 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3101 if ($hold->hold_type eq 'R') {
3102 $copy->status(OILS_COPY_STATUS_CATALOGING);
3103 $hold->fulfillment_time('now');
3104 $self->noop(1); # Block other transit/hold checks
3105 $self->bail_on_events($self->editor->event)
3106 unless $self->editor->update_action_hold_request($hold);
3108 # This hold was captured in the correct location
3109 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3110 $self->push_events(OpenILS::Event->new('SUCCESS'));
3112 #$self->do_hold_notify($hold->id);
3113 $self->notify_hold($hold->id);
3118 # Hold needs to be picked up elsewhere. Build a hold
3119 # transit and route the item.
3120 $self->checkin_build_hold_transit();
3121 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3122 return 0 if $self->bail_out;
3123 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3126 # make sure we save the copy status
3128 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3132 sub attempt_checkin_reservation_capture {
3134 my $copy = $self->copy;
3136 # we've been explicitly told not to capture any holds
3137 return 0 if $self->capture eq 'nocapture';
3139 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3140 my $evt = $booking_ses->request(
3141 "open-ils.booking.resources.capture_for_reservation",
3142 $self->editor->authtoken,
3144 1 # don't update copy - we probably have it locked
3146 $booking_ses->disconnect;
3148 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3150 "open-ils.booking.resources.capture_for_reservation " .
3151 "didn't return an event!"
3155 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3156 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3158 # not-transferable is an error event we'll pass on the user
3159 $logger->warn("reservation capture attempted against non-transferable item");
3160 $self->push_events($evt);
3162 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3163 # Re-retrieve copy as reservation capture may have changed
3164 # its status and whatnot.
3166 "circulator: booking capture win on copy " . $self->copy->id
3168 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3170 "circulator: changing copy " . $self->copy->id .
3171 "'s status from " . $self->copy->status . " to " .
3174 $self->copy->status($new_copy_status);
3177 $self->reservation($evt->{"payload"}->{"reservation"});
3179 if (exists $evt->{"payload"}->{"transit"}) {
3183 "org" => $evt->{"payload"}->{"transit"}->dest
3187 $self->checkin_changed(1);
3191 # other results are treated as "nothing to capture"
3195 sub do_hold_notify {
3196 my( $self, $holdid ) = @_;
3198 my $e = new_editor(xact => 1);
3199 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3201 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3202 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3204 $logger->info("circulator: running delayed hold notify process");
3206 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3207 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3209 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3210 hold_id => $holdid, requestor => $self->editor->requestor);
3212 $logger->debug("circulator: built hold notifier");
3214 if(!$notifier->event) {
3216 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3218 my $stat = $notifier->send_email_notify;
3219 if( $stat == '1' ) {
3220 $logger->info("circulator: hold notify succeeded for hold $holdid");
3224 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3227 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3231 sub retarget_holds {
3233 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3234 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3235 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3236 # no reason to wait for the return value
3240 sub checkin_build_hold_transit {
3243 my $copy = $self->copy;
3244 my $hold = $self->hold;
3245 my $trans = Fieldmapper::action::hold_transit_copy->new;
3247 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3249 $trans->hold($hold->id);
3250 $trans->source($self->circ_lib);
3251 $trans->dest($hold->pickup_lib);
3252 $trans->source_send_time("now");
3253 $trans->target_copy($copy->id);
3255 # when the copy gets to its destination, it will recover
3256 # this status - put it onto the holds shelf
3257 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3259 return $self->bail_on_events($self->editor->event)
3260 unless $self->editor->create_action_hold_transit_copy($trans);
3265 sub process_received_transit {
3267 my $copy = $self->copy;
3268 my $copyid = $self->copy->id;
3270 my $status_name = $U->copy_status($copy->status)->name;
3271 $logger->debug("circulator: attempting transit receive on ".
3272 "copy $copyid. Copy status is $status_name");
3274 my $transit = $self->transit;
3276 # Check if we are in a transit suppress range
3277 my $suppress_transit = 0;
3278 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3279 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3280 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3281 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3282 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3283 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3284 $suppress_transit = 1;
3285 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3289 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3290 # - this item is in-transit to a different location
3291 # - Or we are capturing holds as transits, so why create a new transit?
3293 my $tid = $transit->id;
3294 my $loc = $self->circ_lib;
3295 my $dest = $transit->dest;
3297 $logger->info("circulator: Fowarding transit on copy which is destined ".
3298 "for a different location. transit=$tid, copy=$copyid, current ".
3299 "location=$loc, destination location=$dest");
3301 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3303 # grab the associated hold object if available
3304 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3305 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3307 return $self->bail_on_events($evt);
3310 # The transit is received, set the receive time
3311 $transit->dest_recv_time('now');
3312 $self->bail_on_events($self->editor->event)
3313 unless $self->editor->update_action_transit_copy($transit);
3315 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3317 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3318 $copy->status( $transit->copy_status );
3319 $self->update_copy();
3320 return if $self->bail_out;
3324 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3327 # hold has arrived at destination, set shelf time
3328 $self->put_hold_on_shelf($hold);
3329 $self->bail_on_events($self->editor->event)
3330 unless $self->editor->update_action_hold_request($hold);
3331 return if $self->bail_out;
3333 $self->notify_hold($hold_transit->hold);
3336 $hold_transit = undef;
3337 $self->cancelled_hold_transit(1);
3338 $self->reshelve_copy(1);
3339 $self->fake_hold_dest(0);
3344 OpenILS::Event->new(
3347 payload => { transit => $transit, holdtransit => $hold_transit } ));
3349 return $hold_transit;
3353 # ------------------------------------------------------------------
3354 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3355 # ------------------------------------------------------------------
3356 sub put_hold_on_shelf {
3357 my($self, $hold) = @_;
3358 $hold->shelf_time('now');
3359 $hold->current_shelf_lib($self->circ_lib);
3360 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3368 my $reservation = shift;
3369 my $ignore_stop_fines = shift;
3370 my $dt_parser = DateTime::Format::ISO8601->new;
3372 my $obj = $reservation ? $self->reservation : $self->circ;
3374 return undef if (!$ignore_stop_fines and $obj->stop_fines);
3376 # If we have a grace period
3377 if($obj->can('grace_period')) {
3378 # Parse out the due date
3379 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3380 # Add the grace period to the due date
3381 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3382 # Don't generate fines on circs still in grace period
3383 return undef if ($due_date > DateTime->now);
3386 $CC->generate_fines({circs => [$obj], editor => $self->editor});
3391 sub checkin_handle_circ {
3393 my $circ = $self->circ;
3394 my $copy = $self->copy;
3398 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3400 # backdate the circ if necessary
3401 if($self->backdate) {
3402 my $evt = $self->checkin_handle_backdate;
3403 return $self->bail_on_events($evt) if $evt;
3406 if(!$circ->stop_fines) {
3407 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3408 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3409 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3410 $circ->stop_fines_time('now');
3411 $circ->stop_fines_time($self->backdate) if $self->backdate;
3414 # Set the checkin vars since we have the item
3415 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3417 # capture the true scan time for back-dated checkins
3418 $circ->checkin_scan_time('now');
3420 $circ->checkin_staff($self->editor->requestor->id);
3421 $circ->checkin_lib($self->circ_lib);
3422 $circ->checkin_workstation($self->editor->requestor->wsid);
3424 my $circ_lib = (ref $self->copy->circ_lib) ?
3425 $self->copy->circ_lib->id : $self->copy->circ_lib;
3426 my $stat = $U->copy_status($self->copy->status)->id;
3428 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3429 # we will now handle lost fines, but the copy will retain its 'lost'
3430 # status if it needs to transit home unless lost_immediately_available
3433 # if we decide to also delay fine handling until the item arrives home,
3434 # we will need to call lost fine handling code both when checking items
3435 # in and also when receiving transits
3436 $self->checkin_handle_lost($circ_lib);
3437 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3438 # same process as above.
3439 $self->checkin_handle_long_overdue($circ_lib);
3440 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3441 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3443 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3448 # see if there are any fines owed on this circ. if not, close it
3449 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3450 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3452 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3454 return $self->bail_on_events($self->editor->event)
3455 unless $self->editor->update_action_circulation($circ);
3460 # ------------------------------------------------------------------
3461 # See if we need to void billings, etc. for lost checkin
3462 # ------------------------------------------------------------------
3463 sub checkin_handle_lost {
3465 my $circ_lib = shift;
3467 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3468 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3470 return $self->checkin_handle_lost_or_longoverdue(
3471 circ_lib => $circ_lib,
3472 max_return => $max_return,
3473 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3474 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3475 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3476 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3477 ous_use_last_activity => undef, # not supported for LOST checkin
3478 void_cost_btype => 3,
3483 # ------------------------------------------------------------------
3484 # See if we need to void billings, etc. for long-overdue checkin
3485 # note: not using constants below since they serve little purpose
3486 # for single-use strings that are descriptive in their own right
3487 # and mostly just complicate debugging.
3488 # ------------------------------------------------------------------
3489 sub checkin_handle_long_overdue {
3491 my $circ_lib = shift;
3493 $logger->info("circulator: processing long-overdue checkin...");
3495 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3496 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3498 return $self->checkin_handle_lost_or_longoverdue(
3499 circ_lib => $circ_lib,
3500 max_return => $max_return,
3501 is_longoverdue => 1,
3502 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3503 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3504 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3505 ous_immediately_available => 'circ.longoverdue_immediately_available',
3506 ous_use_last_activity =>
3507 'circ.longoverdue.use_last_activity_date_on_return',
3508 void_cost_btype => 10,
3509 void_fee_btype => 11
3513 # last billing activity is last payment time, last billing time, or the
3514 # circ due date. If the relevant "use last activity" org unit setting is
3515 # false/unset, then last billing activity is always the due date.
3516 sub get_circ_last_billing_activity {
3518 my $circ_lib = shift;
3519 my $setting = shift;
3520 my $date = $self->circ->due_date;
3522 return $date unless $setting and
3523 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3525 my $xact = $self->editor->retrieve_money_billable_transaction([
3527 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3530 if ($xact->summary) {
3531 $date = $xact->summary->last_payment_ts ||
3532 $xact->summary->last_billing_ts ||
3533 $self->circ->due_date;
3540 sub checkin_handle_lost_or_longoverdue {
3541 my ($self, %args) = @_;
3543 my $circ = $self->circ;
3544 my $max_return = $args{max_return};
3545 my $circ_lib = $args{circ_lib};
3550 $self->get_circ_last_billing_activity(
3551 $circ_lib, $args{ous_use_last_activity});
3554 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3555 $tm[5] -= 1 if $tm[5] > 0;
3556 my $due = timelocal(int($tm[1]), int($tm[2]),
3557 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3560 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3562 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3563 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3564 "DUE: $due LAST: $last_chance");
3566 $max_return = 0 if $today < $last_chance;
3572 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3573 "return interval. skipping fine/fee voiding, etc.");
3575 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3577 $logger->info("circulator: check-in of lost/lo item having a balance ".
3578 "of zero, skipping fine/fee voiding and reinstatement.");
3580 } else { # within max-return interval or no interval defined
3582 $logger->info("circulator: check-in of lost/lo item is within the ".
3583 "max return interval (or no interval is defined). Proceeding ".
3584 "with fine/fee voiding, etc.");
3586 my $void_cost = $U->ou_ancestor_setting_value(
3587 $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3588 my $void_proc_fee = $U->ou_ancestor_setting_value(
3589 $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3590 my $restore_od = $U->ou_ancestor_setting_value(
3591 $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3593 $self->checkin_handle_lost_or_lo_now_found(
3594 $args{void_cost_btype}, $args{is_longoverdue}) if ($void_cost);
3595 $self->checkin_handle_lost_or_lo_now_found(
3596 $args{void_fee_btype}, $args{is_longoverdue}) if ($void_proc_fee);
3597 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3598 if $restore_od && ! $self->void_overdues;
3601 if ($circ_lib != $self->circ_lib) {
3602 # if the item is not home, check to see if we want to retain the
3603 # lost/longoverdue status at this point in the process
3605 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3606 $args{ous_immediately_available}, $self->editor) || 0;
3608 if ($immediately_available) {
3609 # item status does not need to be retained, so give it a
3610 # reshelving status as if it were a normal checkin
3611 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3614 $logger->info("circulator: leaving lost/longoverdue copy".
3615 " status in place on checkin");
3618 # lost/longoverdue item is home and processed, treat like a normal
3619 # checkin from this point on
3620 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3626 sub checkin_handle_backdate {
3629 # ------------------------------------------------------------------
3630 # clean up the backdate for date comparison
3631 # XXX We are currently taking the due-time from the original due-date,
3632 # not the input. Do we need to do this? This certainly interferes with
3633 # backdating of hourly checkouts, but that is likely a very rare case.
3634 # ------------------------------------------------------------------
3635 my $bd = cleanse_ISO8601($self->backdate);
3636 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3637 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3638 $new_date->set_hour($original_date->hour());
3639 $new_date->set_minute($original_date->minute());
3640 if ($new_date >= DateTime->now) {
3641 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3644 $bd = cleanse_ISO8601($new_date->datetime());
3647 $self->backdate($bd);
3652 sub check_checkin_copy_status {
3654 my $copy = $self->copy;
3656 my $status = $U->copy_status($copy->status)->id;
3659 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3660 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3661 $status == OILS_COPY_STATUS_IN_PROCESS ||
3662 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3663 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3664 $status == OILS_COPY_STATUS_CATALOGING ||
3665 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3666 $status == OILS_COPY_STATUS_RESHELVING );
3668 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3669 if( $status == OILS_COPY_STATUS_LOST );
3671 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3672 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3674 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3675 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3677 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3678 if( $status == OILS_COPY_STATUS_MISSING );
3680 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3685 # --------------------------------------------------------------------------
3686 # On checkin, we need to return as many relevant objects as we can
3687 # --------------------------------------------------------------------------
3688 sub checkin_flesh_events {
3691 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3692 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3693 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3696 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3699 if($self->hold and !$self->hold->cancel_time) {
3700 $hold = $self->hold;
3701 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3705 # update our copy of the circ object and
3706 # flesh the billing summary data
3708 $self->editor->retrieve_action_circulation([
3712 circ => ['billable_transaction'],
3721 # flesh some patron fields before returning
3723 $self->editor->retrieve_actor_user([
3728 au => ['card', 'billing_address', 'mailing_address']
3735 for my $evt (@{$self->events}) {
3738 $payload->{copy} = $U->unflesh_copy($self->copy);
3739 $payload->{volume} = $self->volume;
3740 $payload->{record} = $record,
3741 $payload->{circ} = $self->circ;
3742 $payload->{transit} = $self->transit;
3743 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3744 $payload->{hold} = $hold;
3745 $payload->{patron} = $self->patron;
3746 $payload->{reservation} = $self->reservation
3747 unless (not $self->reservation or $self->reservation->cancel_time);
3749 $evt->{payload} = $payload;
3754 my( $self, $msg ) = @_;
3755 my $bc = ($self->copy) ? $self->copy->barcode :
3758 my $usr = ($self->patron) ? $self->patron->id : "";
3759 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3760 ", recipient=$usr, copy=$bc");
3766 $self->log_me("do_renew()");
3768 # Make sure there is an open circ to renew that is not
3769 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3770 my $usrid = $self->patron->id if $self->patron;
3771 my $circ = $self->editor->search_action_circulation({
3772 target_copy => $self->copy->id,
3773 xact_finish => undef,
3774 checkin_time => undef,
3775 ($usrid ? (usr => $usrid) : ()),
3777 {stop_fines => undef},
3778 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3782 return $self->bail_on_events($self->editor->event) unless $circ;
3784 # A user is not allowed to renew another user's items without permission
3785 unless( $circ->usr eq $self->editor->requestor->id ) {
3786 return $self->bail_on_events($self->editor->events)
3787 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3790 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3791 if $circ->renewal_remaining < 1;
3793 # -----------------------------------------------------------------
3795 $self->parent_circ($circ->id);
3796 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3799 # Opac renewal - re-use circ library from original circ (unless told not to)
3800 if($self->opac_renewal) {
3801 unless(defined($opac_renewal_use_circ_lib)) {
3802 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3803 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3804 $opac_renewal_use_circ_lib = 1;
3807 $opac_renewal_use_circ_lib = 0;
3810 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3813 # Desk renewal - re-use circ library from original circ (unless told not to)
3814 if($self->desk_renewal) {
3815 unless(defined($desk_renewal_use_circ_lib)) {
3816 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3817 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3818 $desk_renewal_use_circ_lib = 1;
3821 $desk_renewal_use_circ_lib = 0;
3824 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3827 # Run the fine generator against the old circ
3828 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3829 # a few lines down. Commenting out, for now.
3830 #$self->handle_fines;
3832 $self->run_renew_permit;
3835 $self->do_checkin();
3836 return if $self->bail_out;
3838 unless( $self->permit_override ) {
3840 return if $self->bail_out;
3841 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3842 $self->remove_event('ITEM_NOT_CATALOGED');
3845 $self->override_events;
3846 return if $self->bail_out;
3849 $self->do_checkout();
3854 my( $self, $evt ) = @_;
3855 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3856 $logger->debug("circulator: removing event from list: $evt");
3857 my @events = @{$self->events};
3858 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3863 my( $self, $evt ) = @_;
3864 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3865 return grep { $_->{textcode} eq $evt } @{$self->events};
3870 sub run_renew_permit {
3873 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3874 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3875 $self->editor, $self->copy, $self->editor->requestor, 1
3877 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3880 if(!$self->legacy_script_support) {
3881 my $results = $self->run_indb_circ_test;
3882 $self->push_events($self->matrix_test_result_events)
3883 unless $self->circ_test_success;
3886 my $runner = $self->script_runner;
3888 $runner->load($self->circ_permit_renew);
3889 my $result = $runner->run or
3890 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3891 if ($result->{"events"}) {
3893 map { new OpenILS::Event($_) } @{$result->{"events"}}
3896 "circulator: circ_permit_renew for user " .
3897 $self->patron->id . " returned " .
3898 scalar(@{$result->{"events"}}) . " event(s)"
3902 $self->mk_script_runner;
3905 $logger->debug("circulator: re-creating script runner to be safe");
3909 # XXX: The primary mechanism for storing circ history is now handled
3910 # by tracking real circulation objects instead of bibs in a bucket.
3911 # However, this code is disabled by default and could be useful
3912 # some day, so may as well leave it for now.
3913 sub append_reading_list {
3917 $self->is_checkout and
3923 # verify history is globally enabled and uses the bucket mechanism
3924 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3925 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3927 return undef unless $htype and $htype eq 'bucket';
3929 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3931 # verify the patron wants to retain the hisory
3932 my $setting = $e->search_actor_user_setting(
3933 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3935 unless($setting and $setting->value) {
3940 my $bkt = $e->search_container_copy_bucket(
3941 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3946 # find the next item position
3947 my $last_item = $e->search_container_copy_bucket_item(
3948 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3949 $pos = $last_item->pos + 1 if $last_item;
3952 # create the history bucket if necessary
3953 $bkt = Fieldmapper::container::copy_bucket->new;
3954 $bkt->owner($self->patron->id);
3956 $bkt->btype('circ_history');
3958 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3961 my $item = Fieldmapper::container::copy_bucket_item->new;
3963 $item->bucket($bkt->id);
3964 $item->target_copy($self->copy->id);
3967 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3974 sub make_trigger_events {
3976 return unless $self->circ;
3977 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3978 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3979 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3984 sub checkin_handle_lost_or_lo_now_found {
3985 my ($self, $bill_type, $is_longoverdue) = @_;
3987 # ------------------------------------------------------------------
3988 # remove charge from patron's account if lost item is returned
3989 # ------------------------------------------------------------------
3991 my $bills = $self->editor->search_money_billing(
3993 xact => $self->circ->id,
3998 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4000 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
4001 for my $bill (@$bills) {
4002 if( !$U->is_true($bill->voided) ) {
4003 $logger->info("$tag item returned - voiding bill ".$bill->id);
4005 $bill->void_time('now');
4006 $bill->voider($self->editor->requestor->id);
4007 my $note = ($bill->note) ? $bill->note . "\n" : '';
4008 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
4010 $self->bail_on_events($self->editor->event)
4011 unless $self->editor->update_money_billing($bill);
4016 sub checkin_handle_lost_or_lo_now_found_restore_od {
4018 my $circ_lib = shift;
4019 my $is_longoverdue = shift;
4020 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4022 # ------------------------------------------------------------------
4023 # restore those overdue charges voided when item was set to lost
4024 # ------------------------------------------------------------------
4026 my $ods = $self->editor->search_money_billing(
4028 xact => $self->circ->id,
4033 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4034 for my $bill (@$ods) {
4035 if( $U->is_true($bill->voided) ) {
4036 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4038 $bill->clear_void_time;
4039 $bill->voider($self->editor->requestor->id);
4040 my $note = ($bill->note) ? $bill->note . "\n" : '';
4041 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4043 $self->bail_on_events($self->editor->event)
4044 unless $self->editor->update_money_billing($bill);