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
542 needs_lost_bill_handling
548 my $type = ref($self) or die "$self is not an object";
550 my $name = $AUTOLOAD;
553 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
554 $logger->error("circulator: $type: invalid autoload field: $name");
555 die "$type: invalid autoload field: $name\n"
560 *{"${type}::${name}"} = sub {
563 $s->{$name} = $v if defined $v;
567 return $self->$name($data);
572 my( $class, $auth, %args ) = @_;
573 $class = ref($class) || $class;
574 my $self = bless( {}, $class );
577 $self->editor(new_editor(xact => 1, authtoken => $auth));
579 unless( $self->editor->checkauth ) {
580 $self->bail_on_events($self->editor->event);
584 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
586 $self->$_($args{$_}) for keys %args;
589 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
591 # if this is a renewal, default to desk_renewal
592 $self->desk_renewal(1) unless
593 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
595 $self->capture('') unless $self->capture;
597 unless(%user_groups) {
598 my $gps = $self->editor->retrieve_all_permission_grp_tree;
599 %user_groups = map { $_->id => $_ } @$gps;
606 # --------------------------------------------------------------------------
607 # True if we should discontinue processing
608 # --------------------------------------------------------------------------
610 my( $self, $bool ) = @_;
611 if( defined $bool ) {
612 $logger->info("circulator: BAILING OUT") if $bool;
613 $self->{bail_out} = $bool;
615 return $self->{bail_out};
620 my( $self, @evts ) = @_;
623 $e->{payload} = $self->copy if
624 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
626 $logger->info("circulator: pushing event ".$e->{textcode});
627 push( @{$self->events}, $e ) unless
628 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
634 return '' if $self->skip_permit_key;
635 my $key = md5_hex( time() . rand() . "$$" );
636 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
637 return $self->permit_key($key);
640 sub check_permit_key {
642 return 1 if $self->skip_permit_key;
643 my $key = $self->permit_key;
644 return 0 unless $key;
645 my $k = "oils_permit_key_$key";
646 my $one = $self->cache_handle->get_cache($k);
647 $self->cache_handle->delete_cache($k);
648 return ($one) ? 1 : 0;
651 sub seems_like_reservation {
654 # Some words about the following method:
655 # 1) It requires the VIEW_USER permission, but that's not an
656 # issue, right, since all staff should have that?
657 # 2) It returns only one reservation at a time, even if an item can be
658 # and is currently overbooked. Hmmm....
659 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
660 my $result = $booking_ses->request(
661 "open-ils.booking.reservations.by_returnable_resource_barcode",
662 $self->editor->authtoken,
665 $booking_ses->disconnect;
667 return $self->bail_on_events($result) if defined $U->event_code($result);
670 $self->reservation(shift @$result);
678 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
679 sub save_trimmed_copy {
680 my ($self, $copy) = @_;
683 $self->volume($copy->call_number);
684 $self->title($self->volume->record);
685 $self->copy->call_number($self->volume->id);
686 $self->volume->record($self->title->id);
687 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
688 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
689 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
690 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
696 my $e = $self->editor;
698 # --------------------------------------------------------------------------
699 # Grab the fleshed copy
700 # --------------------------------------------------------------------------
701 unless($self->is_noncat) {
704 $copy = $e->retrieve_asset_copy(
705 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
707 } elsif( $self->copy_barcode ) {
709 $copy = $e->search_asset_copy(
710 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
711 } elsif( $self->reservation ) {
712 my $res = $e->json_query(
714 "select" => {"acp" => ["id"]},
719 "field" => "barcode",
723 "field" => "current_resource"
731 "id" => (ref $self->reservation) ?
732 $self->reservation->id : $self->reservation
737 if (ref $res eq "ARRAY" and scalar @$res) {
738 $logger->info("circulator: mapped reservation " .
739 $self->reservation . " to copy " . $res->[0]->{"id"});
740 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
745 $self->save_trimmed_copy($copy);
747 # We can't renew if there is no copy
748 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
749 if $self->is_renewal;
754 # --------------------------------------------------------------------------
756 # --------------------------------------------------------------------------
760 flesh_fields => {au => [ qw/ card / ]}
763 if( $self->patron_id ) {
764 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
765 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
767 } elsif( $self->patron_barcode ) {
769 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
770 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
771 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
773 $patron = $e->retrieve_actor_user($card->usr)
774 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
776 # Use the card we looked up, not the patron's primary, for card active checks
777 $patron->card($card);
780 if( my $copy = $self->copy ) {
783 $flesh->{flesh_fields}->{circ} = ['usr'];
785 my $circ = $e->search_action_circulation([
786 {target_copy => $copy->id, checkin_time => undef}, $flesh
790 $patron = $circ->usr;
791 $circ->usr($patron->id); # de-flesh for consistency
797 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
798 unless $self->patron($patron) or $self->is_checkin;
800 unless($self->is_checkin) {
802 # Check for inactivity and patron reg. expiration
804 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
805 unless $U->is_true($patron->active);
807 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
808 unless $U->is_true($patron->card->active);
810 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
811 cleanse_ISO8601($patron->expire_date));
813 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
814 if( CORE::time > $expire->epoch ) ;
818 # --------------------------------------------------------------------------
819 # This builds the script runner environment and fetches most of the
821 # --------------------------------------------------------------------------
822 sub mk_script_runner {
828 qw/copy copy_barcode copy_id patron
829 patron_id patron_barcode volume title editor/;
831 # Translate our objects into the ScriptBuilder args hash
832 $$args{$_} = $self->$_() for @fields;
834 $args->{ignore_user_status} = 1 if $self->is_checkin;
835 $$args{fetch_patron_by_circ_copy} = 1;
836 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
838 if( my $pco = $self->pending_checkouts ) {
839 $logger->info("circulator: we were given a pending checkouts number of $pco");
840 $$args{patronItemsOut} = $pco;
843 # This fetches most of the objects we need
844 $self->script_runner(
845 OpenILS::Application::Circ::ScriptBuilder->build($args));
847 # Now we translate the ScriptBuilder objects back into self
848 $self->$_($$args{$_}) for @fields;
850 my @evts = @{$args->{_events}} if $args->{_events};
852 $logger->debug("circulator: script builder returned events: @evts") if @evts;
856 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
857 if(!$self->is_noncat and
859 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
863 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
864 return $self->bail_on_events(@e);
869 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
870 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
871 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
872 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
876 # We can't renew if there is no copy
877 return $self->bail_on_events(@evts) if
878 $self->is_renewal and !$self->copy;
880 # Set some circ-specific flags in the script environment
881 my $evt = "environment";
882 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
884 if( $self->is_noncat ) {
885 $self->script_runner->insert("$evt.isNonCat", 1);
886 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
889 if( $self->is_precat ) {
890 $self->script_runner->insert("environment.isPrecat", 1, 1);
893 $self->script_runner->add_path( $_ ) for @$script_libs;
898 # --------------------------------------------------------------------------
899 # Does the circ permit work
900 # --------------------------------------------------------------------------
904 $self->log_me("do_permit()");
906 unless( $self->editor->requestor->id == $self->patron->id ) {
907 return $self->bail_on_events($self->editor->event)
908 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
911 $self->check_captured_holds();
912 $self->do_copy_checks();
913 return if $self->bail_out;
914 $self->run_patron_permit_scripts();
915 $self->run_copy_permit_scripts()
916 unless $self->is_precat or $self->is_noncat;
917 $self->check_item_deposit_events();
918 $self->override_events();
919 return if $self->bail_out;
921 if($self->is_precat and not $self->request_precat) {
924 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
925 return $self->bail_out(1) unless $self->is_renewal;
929 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
932 sub check_item_deposit_events {
934 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
935 if $self->is_deposit and not $self->is_deposit_exempt;
936 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
937 if $self->is_rental and not $self->is_rental_exempt;
940 # returns true if the user is not required to pay deposits
941 sub is_deposit_exempt {
943 my $pid = (ref $self->patron->profile) ?
944 $self->patron->profile->id : $self->patron->profile;
945 my $groups = $U->ou_ancestor_setting_value(
946 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
947 for my $grp (@$groups) {
948 return 1 if $self->is_group_descendant($grp, $pid);
953 # returns true if the user is not required to pay rental fees
954 sub is_rental_exempt {
956 my $pid = (ref $self->patron->profile) ?
957 $self->patron->profile->id : $self->patron->profile;
958 my $groups = $U->ou_ancestor_setting_value(
959 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
960 for my $grp (@$groups) {
961 return 1 if $self->is_group_descendant($grp, $pid);
966 sub is_group_descendant {
967 my($self, $p_id, $c_id) = @_;
968 return 0 unless defined $p_id and defined $c_id;
969 return 1 if $c_id == $p_id;
970 while(my $grp = $user_groups{$c_id}) {
971 $c_id = $grp->parent;
972 return 0 unless defined $c_id;
973 return 1 if $c_id == $p_id;
978 sub check_captured_holds {
980 my $copy = $self->copy;
981 my $patron = $self->patron;
983 return undef unless $copy;
985 my $s = $U->copy_status($copy->status)->id;
986 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
987 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
989 # Item is on the holds shelf, make sure it's going to the right person
990 my $hold = $self->editor->search_action_hold_request(
993 current_copy => $copy->id ,
994 capture_time => { '!=' => undef },
995 cancel_time => undef,
996 fulfillment_time => undef
1002 if ($hold and $hold->usr == $patron->id) {
1003 $self->checkout_is_for_hold(1);
1007 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1009 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1013 sub do_copy_checks {
1015 my $copy = $self->copy;
1016 return unless $copy;
1018 my $stat = $U->copy_status($copy->status)->id;
1020 # We cannot check out a copy if it is in-transit
1021 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1022 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1025 $self->handle_claims_returned();
1026 return if $self->bail_out;
1028 # no claims returned circ was found, check if there is any open circ
1029 unless( $self->is_renewal ) {
1031 my $circs = $self->editor->search_action_circulation(
1032 { target_copy => $copy->id, checkin_time => undef }
1035 if(my $old_circ = $circs->[0]) { # an open circ was found
1037 my $payload = {copy => $copy};
1039 if($old_circ->usr == $self->patron->id) {
1041 $payload->{old_circ} = $old_circ;
1043 # If there is an open circulation on the checkout item and an auto-renew
1044 # interval is defined, inform the caller that they should go
1045 # ahead and renew the item instead of warning about open circulations.
1047 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1049 'circ.checkout_auto_renew_age',
1053 if($auto_renew_intvl) {
1054 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1055 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1057 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1058 $payload->{auto_renew} = 1;
1063 return $self->bail_on_events(
1064 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1070 my $LEGACY_CIRC_EVENT_MAP = {
1071 'no_item' => 'ITEM_NOT_CATALOGED',
1072 'actor.usr.barred' => 'PATRON_BARRED',
1073 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1074 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1075 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1076 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1077 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1078 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1079 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1080 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1081 'config.circ_matrix_test.total_copy_hold_ratio' =>
1082 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
1083 'config.circ_matrix_test.available_copy_hold_ratio' =>
1084 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
1088 # ---------------------------------------------------------------------
1089 # This pushes any patron-related events into the list but does not
1090 # set bail_out for any events
1091 # ---------------------------------------------------------------------
1092 sub run_patron_permit_scripts {
1094 my $runner = $self->script_runner;
1095 my $patronid = $self->patron->id;
1099 if(!$self->legacy_script_support) {
1101 my $results = $self->run_indb_circ_test;
1102 unless($self->circ_test_success) {
1103 my @trimmed_results;
1105 if ($self->is_noncat) {
1106 # no_item result is OK during noncat checkout
1107 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1111 if ($self->checkout_is_for_hold) {
1112 # if this checkout will fulfill a hold, ignore CIRC blocks
1113 # and rely instead on the (later-checked) FULFILL block
1115 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1116 my $fblock_pens = $self->editor->search_config_standing_penalty(
1117 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1119 for my $res (@$results) {
1120 my $name = $res->{fail_part} || '';
1121 next if grep {$_->name eq $name} @$fblock_pens;
1122 push(@trimmed_results, $res);
1126 # not for hold or noncat
1127 @trimmed_results = @$results;
1131 # update the final set of test results
1132 $self->matrix_test_result(\@trimmed_results);
1134 push @allevents, $self->matrix_test_result_events;
1139 # ---------------------------------------------------------------------
1140 # # Now run the patron permit script
1141 # ---------------------------------------------------------------------
1142 $runner->load($self->circ_permit_patron);
1143 my $result = $runner->run or
1144 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1146 my $patron_events = $result->{events};
1148 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1149 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1150 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1151 $penalties = $penalties->{fatal_penalties};
1153 for my $pen (@$penalties) {
1154 # CIRC blocks are ignored if this is a FULFILL scenario
1155 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1156 my $event = OpenILS::Event->new($pen->name);
1157 $event->{desc} = $pen->label;
1158 push(@allevents, $event);
1161 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1165 $_->{payload} = $self->copy if
1166 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1169 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1171 $self->push_events(@allevents);
1174 sub matrix_test_result_codes {
1176 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1179 sub matrix_test_result_events {
1182 my $event = new OpenILS::Event(
1183 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1185 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1187 } (@{$self->matrix_test_result});
1190 sub run_indb_circ_test {
1192 return $self->matrix_test_result if $self->matrix_test_result;
1194 my $dbfunc = ($self->is_renewal) ?
1195 'action.item_user_renew_test' : 'action.item_user_circ_test';
1197 if( $self->is_precat && $self->request_precat) {
1198 $self->make_precat_copy;
1199 return if $self->bail_out;
1202 my $results = $self->editor->json_query(
1206 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1212 $self->circ_test_success($U->is_true($results->[0]->{success}));
1214 if(my $mp = $results->[0]->{matchpoint}) {
1215 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1216 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1217 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1218 if(defined($results->[0]->{renewals})) {
1219 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1221 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1222 if(defined($results->[0]->{grace_period})) {
1223 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1225 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1226 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1227 # Grab the *last* response for limit_groups, where it is more likely to be filled
1228 $self->limit_groups($results->[-1]->{limit_groups});
1231 return $self->matrix_test_result($results);
1234 # ---------------------------------------------------------------------
1235 # given a use and copy, this will calculate the circulation policy
1236 # parameters. Only works with in-db circ.
1237 # ---------------------------------------------------------------------
1241 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1243 $self->run_indb_circ_test;
1246 circ_test_success => $self->circ_test_success,
1247 failure_events => [],
1248 failure_codes => [],
1249 matchpoint => $self->circ_matrix_matchpoint
1252 unless($self->circ_test_success) {
1253 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1254 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1257 if($self->circ_matrix_matchpoint) {
1258 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1259 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1260 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1261 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1263 my $policy = $self->get_circ_policy(
1264 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1266 $$results{$_} = $$policy{$_} for keys %$policy;
1272 # ---------------------------------------------------------------------
1273 # Loads the circ policy info for duration, recurring fine, and max
1274 # fine based on the current copy
1275 # ---------------------------------------------------------------------
1276 sub get_circ_policy {
1277 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1280 duration_rule => $duration_rule->name,
1281 recurring_fine_rule => $recurring_fine_rule->name,
1282 max_fine_rule => $max_fine_rule->name,
1283 max_fine => $self->get_max_fine_amount($max_fine_rule),
1284 fine_interval => $recurring_fine_rule->recurrence_interval,
1285 renewal_remaining => $duration_rule->max_renewals,
1286 grace_period => $recurring_fine_rule->grace_period
1289 if($hard_due_date) {
1290 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1291 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1294 $policy->{duration_date_ceiling} = undef;
1295 $policy->{duration_date_ceiling_force} = undef;
1298 $policy->{duration} = $duration_rule->shrt
1299 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1300 $policy->{duration} = $duration_rule->normal
1301 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1302 $policy->{duration} = $duration_rule->extended
1303 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1305 $policy->{recurring_fine} = $recurring_fine_rule->low
1306 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1307 $policy->{recurring_fine} = $recurring_fine_rule->normal
1308 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1309 $policy->{recurring_fine} = $recurring_fine_rule->high
1310 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1315 sub get_max_fine_amount {
1317 my $max_fine_rule = shift;
1318 my $max_amount = $max_fine_rule->amount;
1320 # if is_percent is true then the max->amount is
1321 # use as a percentage of the copy price
1322 if ($U->is_true($max_fine_rule->is_percent)) {
1323 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1324 $max_amount = $price * $max_fine_rule->amount / 100;
1326 $U->ou_ancestor_setting_value(
1328 'circ.max_fine.cap_at_price',
1332 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1333 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1341 sub run_copy_permit_scripts {
1343 my $copy = $self->copy || return;
1344 my $runner = $self->script_runner;
1348 if(!$self->legacy_script_support) {
1349 my $results = $self->run_indb_circ_test;
1350 push @allevents, $self->matrix_test_result_events
1351 unless $self->circ_test_success;
1354 # ---------------------------------------------------------------------
1355 # Capture all of the copy permit events
1356 # ---------------------------------------------------------------------
1357 $runner->load($self->circ_permit_copy);
1358 my $result = $runner->run or
1359 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1360 my $copy_events = $result->{events};
1362 # ---------------------------------------------------------------------
1363 # Now collect all of the events together
1364 # ---------------------------------------------------------------------
1365 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1368 # See if this copy has an alert message
1369 my $ae = $self->check_copy_alert();
1370 push( @allevents, $ae ) if $ae;
1372 # uniquify the events
1373 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1374 @allevents = values %hash;
1376 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1378 $self->push_events(@allevents);
1382 sub check_copy_alert {
1384 return undef if $self->is_renewal;
1385 return OpenILS::Event->new(
1386 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1387 if $self->copy and $self->copy->alert_message;
1393 # --------------------------------------------------------------------------
1394 # If the call is overriding and has permissions to override every collected
1395 # event, the are cleared. Any event that the caller does not have
1396 # permission to override, will be left in the event list and bail_out will
1398 # XXX We need code in here to cancel any holds/transits on copies
1399 # that are being force-checked out
1400 # --------------------------------------------------------------------------
1401 sub override_events {
1403 my @events = @{$self->events};
1404 return unless @events;
1405 my $oargs = $self->override_args;
1407 if(!$self->override) {
1408 return $self->bail_out(1)
1409 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1414 for my $e (@events) {
1415 my $tc = $e->{textcode};
1416 next if $tc eq 'SUCCESS';
1417 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1418 my $ov = "$tc.override";
1419 $logger->info("circulator: attempting to override event: $ov");
1421 return $self->bail_on_events($self->editor->event)
1422 unless( $self->editor->allowed($ov) );
1424 return $self->bail_out(1);
1430 # --------------------------------------------------------------------------
1431 # If there is an open claimsreturn circ on the requested copy, close the
1432 # circ if overriding, otherwise bail out
1433 # --------------------------------------------------------------------------
1434 sub handle_claims_returned {
1436 my $copy = $self->copy;
1438 my $CR = $self->editor->search_action_circulation(
1440 target_copy => $copy->id,
1441 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1442 checkin_time => undef,
1446 return unless ($CR = $CR->[0]);
1450 # - If the caller has set the override flag, we will check the item in
1451 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1453 $CR->checkin_time('now');
1454 $CR->checkin_scan_time('now');
1455 $CR->checkin_lib($self->circ_lib);
1456 $CR->checkin_workstation($self->editor->requestor->wsid);
1457 $CR->checkin_staff($self->editor->requestor->id);
1459 $evt = $self->editor->event
1460 unless $self->editor->update_action_circulation($CR);
1463 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1466 $self->bail_on_events($evt) if $evt;
1471 # --------------------------------------------------------------------------
1472 # This performs the checkout
1473 # --------------------------------------------------------------------------
1477 $self->log_me("do_checkout()");
1479 # make sure perms are good if this isn't a renewal
1480 unless( $self->is_renewal ) {
1481 return $self->bail_on_events($self->editor->event)
1482 unless( $self->editor->allowed('COPY_CHECKOUT') );
1485 # verify the permit key
1486 unless( $self->check_permit_key ) {
1487 if( $self->permit_override ) {
1488 return $self->bail_on_events($self->editor->event)
1489 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1491 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1495 # if this is a non-cataloged circ, build the circ and finish
1496 if( $self->is_noncat ) {
1497 $self->checkout_noncat;
1499 OpenILS::Event->new('SUCCESS',
1500 payload => { noncat_circ => $self->circ }));
1504 if( $self->is_precat ) {
1505 $self->make_precat_copy;
1506 return if $self->bail_out;
1508 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1509 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1512 $self->do_copy_checks;
1513 return if $self->bail_out;
1515 $self->run_checkout_scripts();
1516 return if $self->bail_out;
1518 $self->build_checkout_circ_object();
1519 return if $self->bail_out;
1521 my $modify_to_start = $self->booking_adjusted_due_date();
1522 return if $self->bail_out;
1524 $self->apply_modified_due_date($modify_to_start);
1525 return if $self->bail_out;
1527 return $self->bail_on_events($self->editor->event)
1528 unless $self->editor->create_action_circulation($self->circ);
1530 # refresh the circ to force local time zone for now
1531 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1533 if($self->limit_groups) {
1534 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1537 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1539 return if $self->bail_out;
1541 $self->apply_deposit_fee();
1542 return if $self->bail_out;
1544 $self->handle_checkout_holds();
1545 return if $self->bail_out;
1547 # ------------------------------------------------------------------------------
1548 # Update the patron penalty info in the DB. Run it for permit-overrides
1549 # since the penalties are not updated during the permit phase
1550 # ------------------------------------------------------------------------------
1551 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1553 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1556 if($self->is_renewal) {
1557 # flesh the billing summary for the checked-in circ
1558 $pcirc = $self->editor->retrieve_action_circulation([
1560 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1565 OpenILS::Event->new('SUCCESS',
1567 copy => $U->unflesh_copy($self->copy),
1568 volume => $self->volume,
1569 circ => $self->circ,
1571 holds_fulfilled => $self->fulfilled_holds,
1572 deposit_billing => $self->deposit_billing,
1573 rental_billing => $self->rental_billing,
1574 parent_circ => $pcirc,
1575 patron => ($self->return_patron) ? $self->patron : undef,
1576 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1582 sub apply_deposit_fee {
1584 my $copy = $self->copy;
1586 ($self->is_deposit and not $self->is_deposit_exempt) or
1587 ($self->is_rental and not $self->is_rental_exempt);
1589 return if $self->is_deposit and $self->skip_deposit_fee;
1590 return if $self->is_rental and $self->skip_rental_fee;
1592 my $bill = Fieldmapper::money::billing->new;
1593 my $amount = $copy->deposit_amount;
1597 if($self->is_deposit) {
1598 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1600 $self->deposit_billing($bill);
1602 $billing_type = OILS_BILLING_TYPE_RENTAL;
1604 $self->rental_billing($bill);
1607 $bill->xact($self->circ->id);
1608 $bill->amount($amount);
1609 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1610 $bill->billing_type($billing_type);
1611 $bill->btype($btype);
1612 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1614 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1619 my $copy = $self->copy;
1621 my $stat = $copy->status if ref $copy->status;
1622 my $loc = $copy->location if ref $copy->location;
1623 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1625 $copy->status($stat->id) if $stat;
1626 $copy->location($loc->id) if $loc;
1627 $copy->circ_lib($circ_lib->id) if $circ_lib;
1628 $copy->editor($self->editor->requestor->id);
1629 $copy->edit_date('now');
1630 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1632 return $self->bail_on_events($self->editor->event)
1633 unless $self->editor->update_asset_copy($self->copy);
1635 $copy->status($U->copy_status($copy->status));
1636 $copy->location($loc) if $loc;
1637 $copy->circ_lib($circ_lib) if $circ_lib;
1640 sub update_reservation {
1642 my $reservation = $self->reservation;
1644 my $usr = $reservation->usr;
1645 my $target_rt = $reservation->target_resource_type;
1646 my $target_r = $reservation->target_resource;
1647 my $current_r = $reservation->current_resource;
1649 $reservation->usr($usr->id) if ref $usr;
1650 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1651 $reservation->target_resource($target_r->id) if ref $target_r;
1652 $reservation->current_resource($current_r->id) if ref $current_r;
1654 return $self->bail_on_events($self->editor->event)
1655 unless $self->editor->update_booking_reservation($self->reservation);
1658 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1659 $self->reservation($reservation);
1663 sub bail_on_events {
1664 my( $self, @evts ) = @_;
1665 $self->push_events(@evts);
1669 # ------------------------------------------------------------------------------
1670 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1671 # affects copies that will fulfill holds and CIRC affects all other copies.
1672 # If blocks exists, bail, push Events onto the event pile, and return true.
1673 # ------------------------------------------------------------------------------
1674 sub check_hold_fulfill_blocks {
1677 # See if the user has any penalties applied that prevent hold fulfillment
1678 my $pens = $self->editor->json_query({
1679 select => {csp => ['name', 'label']},
1680 from => {ausp => {csp => {}}},
1683 usr => $self->patron->id,
1684 org_unit => $U->get_org_full_path($self->circ_lib),
1686 {stop_date => undef},
1687 {stop_date => {'>' => 'now'}}
1690 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1694 return 0 unless @$pens;
1696 for my $pen (@$pens) {
1697 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1698 my $event = OpenILS::Event->new($pen->{name});
1699 $event->{desc} = $pen->{label};
1700 $self->push_events($event);
1703 $self->override_events;
1704 return $self->bail_out;
1708 # ------------------------------------------------------------------------------
1709 # When an item is checked out, see if we can fulfill a hold for this patron
1710 # ------------------------------------------------------------------------------
1711 sub handle_checkout_holds {
1713 my $copy = $self->copy;
1714 my $patron = $self->patron;
1716 my $e = $self->editor;
1717 $self->fulfilled_holds([]);
1719 # non-cats can't fulfill a hold
1720 return if $self->is_noncat;
1722 my $hold = $e->search_action_hold_request({
1723 current_copy => $copy->id ,
1724 cancel_time => undef,
1725 fulfillment_time => undef,
1727 {expire_time => undef},
1728 {expire_time => {'>' => 'now'}}
1732 if($hold and $hold->usr != $patron->id) {
1733 # reset the hold since the copy is now checked out
1735 $logger->info("circulator: un-targeting hold ".$hold->id.
1736 " because copy ".$copy->id." is getting checked out");
1738 $hold->clear_prev_check_time;
1739 $hold->clear_current_copy;
1740 $hold->clear_capture_time;
1741 $hold->clear_shelf_time;
1742 $hold->clear_shelf_expire_time;
1743 $hold->clear_current_shelf_lib;
1745 return $self->bail_on_event($e->event)
1746 unless $e->update_action_hold_request($hold);
1752 $hold = $self->find_related_user_hold($copy, $patron) or return;
1753 $logger->info("circulator: found related hold to fulfill in checkout");
1756 return if $self->check_hold_fulfill_blocks;
1758 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1760 # if the hold was never officially captured, capture it.
1761 $hold->current_copy($copy->id);
1762 $hold->capture_time('now') unless $hold->capture_time;
1763 $hold->fulfillment_time('now');
1764 $hold->fulfillment_staff($e->requestor->id);
1765 $hold->fulfillment_lib($self->circ_lib);
1767 return $self->bail_on_events($e->event)
1768 unless $e->update_action_hold_request($hold);
1770 return $self->fulfilled_holds([$hold->id]);
1774 # ------------------------------------------------------------------------------
1775 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1776 # the patron directly targets the checked out item, see if there is another hold
1777 # for the patron that could be fulfilled by the checked out item. Fulfill the
1778 # oldest hold and only fulfill 1 of them.
1780 # For "another hold":
1782 # First, check for one that the copy matches via hold_copy_map, ensuring that
1783 # *any* hold type that this copy could fill may end up filled.
1785 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1786 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1787 # that are non-requestable to count as capturing those hold types.
1788 # ------------------------------------------------------------------------------
1789 sub find_related_user_hold {
1790 my($self, $copy, $patron) = @_;
1791 my $e = $self->editor;
1793 # holds on precat copies are always copy-level, so this call will
1794 # always return undef. Exit early.
1795 return undef if $self->is_precat;
1797 return undef unless $U->ou_ancestor_setting_value(
1798 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1800 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1802 select => {ahr => ['id']},
1811 fkey => 'current_copy',
1812 type => 'left' # there may be no current_copy
1819 fulfillment_time => undef,
1820 cancel_time => undef,
1822 {expire_time => undef},
1823 {expire_time => {'>' => 'now'}}
1827 target_copy => $self->copy->id
1831 {id => undef}, # left-join copy may be nonexistent
1832 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1836 order_by => {ahr => {request_time => {direction => 'asc'}}},
1840 my $hold_info = $e->json_query($args)->[0];
1841 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1842 return undef if $U->ou_ancestor_setting_value(
1843 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1845 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1847 select => {ahr => ['id']},
1852 fkey => 'current_copy',
1853 type => 'left' # there may be no current_copy
1860 fulfillment_time => undef,
1861 cancel_time => undef,
1863 {expire_time => undef},
1864 {expire_time => {'>' => 'now'}}
1871 target => $self->volume->id
1877 target => $self->title->id
1883 {id => undef}, # left-join copy may be nonexistent
1884 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1888 order_by => {ahr => {request_time => {direction => 'asc'}}},
1892 $hold_info = $e->json_query($args)->[0];
1893 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1898 sub run_checkout_scripts {
1903 my $runner = $self->script_runner;
1912 my $hard_due_date_name;
1914 if(!$self->legacy_script_support) {
1915 $self->run_indb_circ_test();
1916 $duration = $self->circ_matrix_matchpoint->duration_rule;
1917 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1918 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1919 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1923 $runner->load($self->circ_duration);
1925 my $result = $runner->run or
1926 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1928 $duration_name = $result->{durationRule};
1929 $recurring_name = $result->{recurringFinesRule};
1930 $max_fine_name = $result->{maxFine};
1931 $hard_due_date_name = $result->{hardDueDate};
1934 $duration_name = $duration->name if $duration;
1935 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1938 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1939 return $self->bail_on_events($evt) if ($evt && !$nobail);
1941 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1942 return $self->bail_on_events($evt) if ($evt && !$nobail);
1944 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1945 return $self->bail_on_events($evt) if ($evt && !$nobail);
1947 if($hard_due_date_name) {
1948 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1949 return $self->bail_on_events($evt) if ($evt && !$nobail);
1955 # The item circulates with an unlimited duration
1959 $hard_due_date = undef;
1962 $self->duration_rule($duration);
1963 $self->recurring_fines_rule($recurring);
1964 $self->max_fine_rule($max_fine);
1965 $self->hard_due_date($hard_due_date);
1969 sub build_checkout_circ_object {
1972 my $circ = Fieldmapper::action::circulation->new;
1973 my $duration = $self->duration_rule;
1974 my $max = $self->max_fine_rule;
1975 my $recurring = $self->recurring_fines_rule;
1976 my $hard_due_date = $self->hard_due_date;
1977 my $copy = $self->copy;
1978 my $patron = $self->patron;
1979 my $duration_date_ceiling;
1980 my $duration_date_ceiling_force;
1984 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1985 $duration_date_ceiling = $policy->{duration_date_ceiling};
1986 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1988 my $dname = $duration->name;
1989 my $mname = $max->name;
1990 my $rname = $recurring->name;
1992 if($hard_due_date) {
1993 $hdname = $hard_due_date->name;
1996 $logger->debug("circulator: building circulation ".
1997 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1999 $circ->duration($policy->{duration});
2000 $circ->recurring_fine($policy->{recurring_fine});
2001 $circ->duration_rule($duration->name);
2002 $circ->recurring_fine_rule($recurring->name);
2003 $circ->max_fine_rule($max->name);
2004 $circ->max_fine($policy->{max_fine});
2005 $circ->fine_interval($recurring->recurrence_interval);
2006 $circ->renewal_remaining($duration->max_renewals);
2007 $circ->grace_period($policy->{grace_period});
2011 $logger->info("circulator: copy found with an unlimited circ duration");
2012 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2013 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2014 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2015 $circ->renewal_remaining(0);
2016 $circ->grace_period(0);
2019 $circ->target_copy( $copy->id );
2020 $circ->usr( $patron->id );
2021 $circ->circ_lib( $self->circ_lib );
2022 $circ->workstation($self->editor->requestor->wsid)
2023 if defined $self->editor->requestor->wsid;
2025 # renewals maintain a link to the parent circulation
2026 $circ->parent_circ($self->parent_circ);
2028 if( $self->is_renewal ) {
2029 $circ->opac_renewal('t') if $self->opac_renewal;
2030 $circ->phone_renewal('t') if $self->phone_renewal;
2031 $circ->desk_renewal('t') if $self->desk_renewal;
2032 $circ->renewal_remaining($self->renewal_remaining);
2033 $circ->circ_staff($self->editor->requestor->id);
2037 # if the user provided an overiding checkout time,
2038 # (e.g. the checkout really happened several hours ago), then
2039 # we apply that here. Does this need a perm??
2040 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2041 if $self->checkout_time;
2043 # if a patron is renewing, 'requestor' will be the patron
2044 $circ->circ_staff($self->editor->requestor->id);
2045 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2050 sub do_reservation_pickup {
2053 $self->log_me("do_reservation_pickup()");
2055 $self->reservation->pickup_time('now');
2058 $self->reservation->current_resource &&
2059 $U->is_true($self->reservation->target_resource_type->catalog_item)
2061 # We used to try to set $self->copy and $self->patron here,
2062 # but that should already be done.
2064 $self->run_checkout_scripts(1);
2066 my $duration = $self->duration_rule;
2067 my $max = $self->max_fine_rule;
2068 my $recurring = $self->recurring_fines_rule;
2070 if ($duration && $max && $recurring) {
2071 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2073 my $dname = $duration->name;
2074 my $mname = $max->name;
2075 my $rname = $recurring->name;
2077 $logger->debug("circulator: updating reservation ".
2078 "with duration=$dname, maxfine=$mname, recurring=$rname");
2080 $self->reservation->fine_amount($policy->{recurring_fine});
2081 $self->reservation->max_fine($policy->{max_fine});
2082 $self->reservation->fine_interval($recurring->recurrence_interval);
2085 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2086 $self->update_copy();
2089 $self->reservation->fine_amount(
2090 $self->reservation->target_resource_type->fine_amount
2092 $self->reservation->max_fine(
2093 $self->reservation->target_resource_type->max_fine
2095 $self->reservation->fine_interval(
2096 $self->reservation->target_resource_type->fine_interval
2100 $self->update_reservation();
2103 sub do_reservation_return {
2105 my $request = shift;
2107 $self->log_me("do_reservation_return()");
2109 if (not ref $self->reservation) {
2110 my ($reservation, $evt) =
2111 $U->fetch_booking_reservation($self->reservation);
2112 return $self->bail_on_events($evt) if $evt;
2113 $self->reservation($reservation);
2116 $self->handle_fines(1);
2117 $self->reservation->return_time('now');
2118 $self->update_reservation();
2119 $self->reshelve_copy if $self->copy;
2121 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2122 $self->copy( $self->reservation->current_resource->catalog_item );
2126 sub booking_adjusted_due_date {
2128 my $circ = $self->circ;
2129 my $copy = $self->copy;
2131 return undef unless $self->use_booking;
2135 if( $self->due_date ) {
2137 return $self->bail_on_events($self->editor->event)
2138 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2140 $circ->due_date(cleanse_ISO8601($self->due_date));
2144 return unless $copy and $circ->due_date;
2147 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2148 if (@$booking_items) {
2149 my $booking_item = $booking_items->[0];
2150 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2152 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2153 my $shorten_circ_setting = $resource_type->elbow_room ||
2154 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2157 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2158 my $bookings = $booking_ses->request(
2159 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2160 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2162 $booking_ses->disconnect;
2164 my $dt_parser = DateTime::Format::ISO8601->new;
2165 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2167 for my $bid (@$bookings) {
2169 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2171 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2172 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2174 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2175 if ($booking_start < DateTime->now);
2178 if ($U->is_true($stop_circ_setting)) {
2179 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2181 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2182 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2185 # We set the circ duration here only to affect the logic that will
2186 # later (in a DB trigger) mangle the time part of the due date to
2187 # 11:59pm. Having any circ duration that is not a whole number of
2188 # days is enough to prevent the "correction."
2189 my $new_circ_duration = $due_date->epoch - time;
2190 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2191 $circ->duration("$new_circ_duration seconds");
2193 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2197 return $self->bail_on_events($self->editor->event)
2198 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2204 sub apply_modified_due_date {
2206 my $shift_earlier = shift;
2207 my $circ = $self->circ;
2208 my $copy = $self->copy;
2210 if( $self->due_date ) {
2212 return $self->bail_on_events($self->editor->event)
2213 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2215 $circ->due_date(cleanse_ISO8601($self->due_date));
2219 # if the due_date lands on a day when the location is closed
2220 return unless $copy and $circ->due_date;
2222 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2224 # due-date overlap should be determined by the location the item
2225 # is checked out from, not the owning or circ lib of the item
2226 my $org = $self->circ_lib;
2228 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2229 " with an item due date of ".$circ->due_date );
2231 my $dateinfo = $U->storagereq(
2232 'open-ils.storage.actor.org_unit.closed_date.overlap',
2233 $org, $circ->due_date );
2236 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2237 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2239 # XXX make the behavior more dynamic
2240 # for now, we just push the due date to after the close date
2241 if ($shift_earlier) {
2242 $circ->due_date($dateinfo->{start});
2244 $circ->due_date($dateinfo->{end});
2252 sub create_due_date {
2253 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2255 # if there is a raw time component (e.g. from postgres),
2256 # turn it into an interval that interval_to_seconds can parse
2257 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2259 # for now, use the server timezone. TODO: use workstation org timezone
2260 my $due_date = DateTime->now(time_zone => 'local');
2261 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2263 # add the circ duration
2264 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2267 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2268 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2269 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2274 # return ISO8601 time with timezone
2275 return $due_date->strftime('%FT%T%z');
2280 sub make_precat_copy {
2282 my $copy = $self->copy;
2285 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2287 $copy->editor($self->editor->requestor->id);
2288 $copy->edit_date('now');
2289 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2290 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2291 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2292 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2293 $self->update_copy();
2297 $logger->info("circulator: Creating a new precataloged ".
2298 "copy in checkout with barcode " . $self->copy_barcode);
2300 $copy = Fieldmapper::asset::copy->new;
2301 $copy->circ_lib($self->circ_lib);
2302 $copy->creator($self->editor->requestor->id);
2303 $copy->editor($self->editor->requestor->id);
2304 $copy->barcode($self->copy_barcode);
2305 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2306 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2307 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2309 $copy->dummy_title($self->dummy_title || "");
2310 $copy->dummy_author($self->dummy_author || "");
2311 $copy->dummy_isbn($self->dummy_isbn || "");
2312 $copy->circ_modifier($self->circ_modifier);
2315 # See if we need to override the circ_lib for the copy with a configured circ_lib
2316 # Setting is shortname of the org unit
2317 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2318 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2320 if($precat_circ_lib) {
2321 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2324 $self->bail_on_events($self->editor->event);
2328 $copy->circ_lib($org->id);
2332 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2334 $self->push_events($self->editor->event);
2338 # this is a little bit of a hack, but we need to
2339 # get the copy into the script runner
2340 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2344 sub checkout_noncat {
2350 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2351 my $count = $self->noncat_count || 1;
2352 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2354 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2358 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2359 $self->editor->requestor->id,
2367 $self->push_events($evt);
2375 # If a copy goes into transit and is then checked in before the transit checkin
2376 # interval has expired, push an event onto the overridable events list.
2377 sub check_transit_checkin_interval {
2380 # only concerned with in-transit items
2381 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2383 # no interval, no problem
2384 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2385 return unless $interval;
2387 # capture the transit so we don't have to fetch it again later during checkin
2389 $self->editor->search_action_transit_copy(
2390 {target_copy => $self->copy->id, dest_recv_time => undef}
2394 # transit from X to X for whatever reason has no min interval
2395 return if $self->transit->source == $self->transit->dest;
2397 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2398 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2399 my $horizon = $t_start->add(seconds => $seconds);
2401 # See if we are still within the transit checkin forbidden range
2402 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2403 if $horizon > DateTime->now;
2406 # Retarget local holds at checkin
2407 sub checkin_retarget {
2409 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2410 return unless $self->is_checkin; # Renewals need not be checked
2411 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2412 return if $self->is_precat; # No holds for precats
2413 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2414 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2415 my $status = $U->copy_status($self->copy->status);
2416 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2417 # Specifically target items that are likely new (by status ID)
2418 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2419 my $location = $self->copy->location;
2420 if(!ref($location)) {
2421 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2422 $self->copy->location($location);
2424 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2426 # Fetch holds for the bib
2427 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2428 $self->editor->authtoken,
2431 capture_time => undef, # No touching captured holds
2432 frozen => 'f', # Don't bother with frozen holds
2433 pickup_lib => $self->circ_lib # Only holds actually here
2436 # Error? Skip the step.
2437 return if exists $result->{"ilsevent"};
2441 foreach my $holdlist (keys %{$result}) {
2442 push @$holds, @{$result->{$holdlist}};
2445 return if scalar(@$holds) == 0; # No holds, no retargeting
2447 # Check for parts on this copy
2448 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2449 my %parts_hash = ();
2450 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2452 # Loop over holds in request-ish order
2453 # Stage 1: Get them into request-ish order
2454 # Also grab type and target for skipping low hanging ones
2455 $result = $self->editor->json_query({
2456 "select" => { "ahr" => ["id", "hold_type", "target"] },
2457 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2458 "where" => { "id" => $holds },
2460 { "class" => "pgt", "field" => "hold_priority"},
2461 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2462 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2463 { "class" => "ahr", "field" => "request_time"}
2468 if (ref $result eq "ARRAY" and scalar @$result) {
2469 foreach (@{$result}) {
2470 # Copy level, but not this copy?
2471 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2472 and $_->{target} != $self->copy->id);
2473 # Volume level, but not this volume?
2474 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2475 if(@$parts) { # We have parts?
2477 next if ($_->{hold_type} eq 'T');
2478 # Skip part holds for parts not on this copy
2479 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2481 # No parts, no part holds
2482 next if ($_->{hold_type} eq 'P');
2484 # So much for easy stuff, attempt a retarget!
2485 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2486 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2487 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2495 $self->log_me("do_checkin()");
2497 return $self->bail_on_events(
2498 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2501 $self->check_transit_checkin_interval;
2502 $self->checkin_retarget;
2504 # the renew code and mk_env should have already found our circulation object
2505 unless( $self->circ ) {
2507 my $circs = $self->editor->search_action_circulation(
2508 { target_copy => $self->copy->id, checkin_time => undef });
2510 $self->circ($$circs[0]);
2512 # for now, just warn if there are multiple open circs on a copy
2513 $logger->warn("circulator: we have ".scalar(@$circs).
2514 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2517 my $stat = $U->copy_status($self->copy->status)->id;
2519 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2520 # differently if they are already paid for. We need to check for this
2521 # early since overdue generation is potentially affected.
2522 my $dont_change_lost_zero = 0;
2523 if ($stat == OILS_COPY_STATUS_LOST
2524 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2525 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2527 # LOST fine settings are controlled by the copy's circ lib, not the the
2529 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2530 $self->copy->circ_lib->id : $self->copy->circ_lib;
2531 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2532 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2533 $self->editor) || 0;
2535 if ($dont_change_lost_zero) {
2536 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2537 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2540 $self->dont_change_lost_zero($dont_change_lost_zero);
2543 if( $self->checkin_check_holds_shelf() ) {
2544 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2545 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2546 if($self->fake_hold_dest) {
2547 $self->hold->pickup_lib($self->circ_lib);
2549 $self->checkin_flesh_events;
2553 unless( $self->is_renewal ) {
2554 return $self->bail_on_events($self->editor->event)
2555 unless $self->editor->allowed('COPY_CHECKIN');
2558 $self->push_events($self->check_copy_alert());
2559 $self->push_events($self->check_checkin_copy_status());
2561 # if the circ is marked as 'claims returned', add the event to the list
2562 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2563 if ($self->circ and $self->circ->stop_fines
2564 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2566 $self->check_circ_deposit();
2568 # handle the overridable events
2569 $self->override_events unless $self->is_renewal;
2570 return if $self->bail_out;
2572 if( $self->copy and !$self->transit ) {
2574 $self->editor->search_action_transit_copy(
2575 { target_copy => $self->copy->id, dest_recv_time => undef }
2581 $self->checkin_handle_circ;
2582 return if $self->bail_out;
2583 $self->checkin_changed(1);
2585 if (!$dont_change_lost_zero) {
2586 # if this circ is LOST and we are configured to generate overdue
2587 # fines for lost items on checkin (to fill the gap between mark
2588 # lost time and when the fines would have naturally stopped), then
2589 # stop_fines is no longer valid and should be cleared.
2591 # stop_fines will be set again during the handle_fines() stage.
2592 # XXX should this setting come from the copy circ lib (like other
2593 # LOST settings), instead of the circulation circ lib?
2594 if ($stat == OILS_COPY_STATUS_LOST) {
2595 $self->circ->clear_stop_fines if
2596 $U->ou_ancestor_setting_value(
2598 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2603 # handle fines for this circ, including overdue gen if needed
2604 $self->handle_fines;
2606 } elsif( $self->transit ) {
2607 my $hold_transit = $self->process_received_transit;
2608 $self->checkin_changed(1);
2610 if( $self->bail_out ) {
2611 $self->checkin_flesh_events;
2615 if( my $e = $self->check_checkin_copy_status() ) {
2616 # If the original copy status is special, alert the caller
2617 my $ev = $self->events;
2618 $self->events([$e]);
2619 $self->override_events;
2620 return if $self->bail_out;
2624 if( $hold_transit or
2625 $U->copy_status($self->copy->status)->id
2626 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2629 if( $hold_transit ) {
2630 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2632 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2637 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2639 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2640 $self->reshelve_copy(1);
2641 $self->cancelled_hold_transit(1);
2642 $self->notify_hold(0); # don't notify for cancelled holds
2643 $self->fake_hold_dest(0);
2644 return if $self->bail_out;
2646 } elsif ($hold and $hold->hold_type eq 'R') {
2648 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2649 $self->notify_hold(0); # No need to notify
2650 $self->fake_hold_dest(0);
2651 $self->noop(1); # Don't try and capture for other holds/transits now
2652 $self->update_copy();
2653 $hold->fulfillment_time('now');
2654 $self->bail_on_events($self->editor->event)
2655 unless $self->editor->update_action_hold_request($hold);
2659 # hold transited to correct location
2660 if($self->fake_hold_dest) {
2661 $hold->pickup_lib($self->circ_lib);
2663 $self->checkin_flesh_events;
2668 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2670 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2671 " that is in-transit, but there is no transit.. repairing");
2672 $self->reshelve_copy(1);
2673 return if $self->bail_out;
2676 if( $self->is_renewal ) {
2677 $self->finish_fines_and_voiding;
2678 return if $self->bail_out;
2679 $self->push_events(OpenILS::Event->new('SUCCESS'));
2683 # ------------------------------------------------------------------------------
2684 # Circulations and transits are now closed where necessary. Now go on to see if
2685 # this copy can fulfill a hold or needs to be routed to a different location
2686 # ------------------------------------------------------------------------------
2688 my $needed_for_something = 0; # formerly "needed_for_hold"
2690 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2692 if (!$self->remote_hold) {
2693 if ($self->use_booking) {
2694 my $potential_hold = $self->hold_capture_is_possible;
2695 my $potential_reservation = $self->reservation_capture_is_possible;
2697 if ($potential_hold and $potential_reservation) {
2698 $logger->info("circulator: item could fulfill either hold or reservation");
2699 $self->push_events(new OpenILS::Event(
2700 "HOLD_RESERVATION_CONFLICT",
2701 "hold" => $potential_hold,
2702 "reservation" => $potential_reservation
2704 return if $self->bail_out;
2705 } elsif ($potential_hold) {
2706 $needed_for_something =
2707 $self->attempt_checkin_hold_capture;
2708 } elsif ($potential_reservation) {
2709 $needed_for_something =
2710 $self->attempt_checkin_reservation_capture;
2713 $needed_for_something = $self->attempt_checkin_hold_capture;
2716 return if $self->bail_out;
2718 unless($needed_for_something) {
2719 my $circ_lib = (ref $self->copy->circ_lib) ?
2720 $self->copy->circ_lib->id : $self->copy->circ_lib;
2722 if( $self->remote_hold ) {
2723 $circ_lib = $self->remote_hold->pickup_lib;
2724 $logger->warn("circulator: Copy ".$self->copy->barcode.
2725 " is on a remote hold's shelf, sending to $circ_lib");
2728 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2730 my $suppress_transit = 0;
2732 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2733 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2734 if($suppress_transit_source && $suppress_transit_source->{value}) {
2735 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2736 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2737 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2738 $suppress_transit = 1;
2743 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2744 # copy is where it needs to be, either for hold or reshelving
2746 $self->checkin_handle_precat();
2747 return if $self->bail_out;
2750 # copy needs to transit "home", or stick here if it's a floating copy
2752 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2753 my $res = $self->editor->json_query(
2755 'evergreen.can_float',
2756 $self->copy->floating->id,
2757 $self->copy->circ_lib,
2762 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2764 if ($can_float) { # Yep, floating, stick here
2765 $self->checkin_changed(1);
2766 $self->copy->circ_lib( $self->circ_lib );
2769 my $bc = $self->copy->barcode;
2770 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2771 $self->checkin_build_copy_transit($circ_lib);
2772 return if $self->bail_out;
2773 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2777 } else { # no-op checkin
2778 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2779 $self->checkin_changed(1);
2780 $self->copy->circ_lib( $self->circ_lib );
2785 if($self->claims_never_checked_out and
2786 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2788 # the item was not supposed to be checked out to the user and should now be marked as missing
2789 $self->copy->status(OILS_COPY_STATUS_MISSING);
2793 $self->reshelve_copy unless $needed_for_something;
2796 return if $self->bail_out;
2798 unless($self->checkin_changed) {
2800 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2801 my $stat = $U->copy_status($self->copy->status)->id;
2803 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2804 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2805 $self->bail_out(1); # no need to commit anything
2809 $self->push_events(OpenILS::Event->new('SUCCESS'))
2810 unless @{$self->events};
2813 $self->finish_fines_and_voiding;
2815 OpenILS::Utils::Penalty->calculate_penalties(
2816 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2818 $self->checkin_flesh_events;
2822 sub finish_fines_and_voiding {
2824 return unless $self->circ;
2826 return unless $self->backdate or $self->void_overdues;
2828 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2829 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2831 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2832 $self->editor, $self->circ, $self->backdate, $note);
2834 return $self->bail_on_events($evt) if $evt;
2836 # Make sure the circ is open or closed as necessary.
2837 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2838 return $self->bail_on_events($evt) if $evt;
2844 # if a deposit was payed for this item, push the event
2845 sub check_circ_deposit {
2847 return unless $self->circ;
2848 my $deposit = $self->editor->search_money_billing(
2850 xact => $self->circ->id,
2852 }, {idlist => 1})->[0];
2854 $self->push_events(OpenILS::Event->new(
2855 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2860 my $force = $self->force || shift;
2861 my $copy = $self->copy;
2863 my $stat = $U->copy_status($copy->status)->id;
2866 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2867 $stat != OILS_COPY_STATUS_CATALOGING and
2868 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2869 $stat != OILS_COPY_STATUS_RESHELVING )) {
2871 $copy->status( OILS_COPY_STATUS_RESHELVING );
2873 $self->checkin_changed(1);
2878 # Returns true if the item is at the current location
2879 # because it was transited there for a hold and the
2880 # hold has not been fulfilled
2881 sub checkin_check_holds_shelf {
2883 return 0 unless $self->copy;
2886 $U->copy_status($self->copy->status)->id ==
2887 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2889 # Attempt to clear shelf expired holds for this copy
2890 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2891 if($self->clear_expired);
2893 # find the hold that put us on the holds shelf
2894 my $holds = $self->editor->search_action_hold_request(
2896 current_copy => $self->copy->id,
2897 capture_time => { '!=' => undef },
2898 fulfillment_time => undef,
2899 cancel_time => undef,
2904 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2905 $self->reshelve_copy(1);
2909 my $hold = $$holds[0];
2911 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2912 $hold->id. "] for copy ".$self->copy->barcode);
2914 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2915 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2916 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2917 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2918 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2919 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2920 $self->fake_hold_dest(1);
2926 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2927 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2931 $logger->info("circulator: hold is not for here..");
2932 $self->remote_hold($hold);
2937 sub checkin_handle_precat {
2939 my $copy = $self->copy;
2941 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2942 $copy->status(OILS_COPY_STATUS_CATALOGING);
2943 $self->update_copy();
2944 $self->checkin_changed(1);
2945 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2950 sub checkin_build_copy_transit {
2953 my $copy = $self->copy;
2954 my $transit = Fieldmapper::action::transit_copy->new;
2956 # if we are transiting an item to the shelf shelf, it's a hold transit
2957 if (my $hold = $self->remote_hold) {
2958 $transit = Fieldmapper::action::hold_transit_copy->new;
2959 $transit->hold($hold->id);
2961 # the item is going into transit, remove any shelf-iness
2962 if ($hold->current_shelf_lib or $hold->shelf_time) {
2963 $hold->clear_current_shelf_lib;
2964 $hold->clear_shelf_time;
2965 return $self->bail_on_events($self->editor->event)
2966 unless $self->editor->update_action_hold_request($hold);
2970 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2971 $logger->info("circulator: transiting copy to $dest");
2973 $transit->source($self->circ_lib);
2974 $transit->dest($dest);
2975 $transit->target_copy($copy->id);
2976 $transit->source_send_time('now');
2977 $transit->copy_status( $U->copy_status($copy->status)->id );
2979 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2981 if ($self->remote_hold) {
2982 return $self->bail_on_events($self->editor->event)
2983 unless $self->editor->create_action_hold_transit_copy($transit);
2985 return $self->bail_on_events($self->editor->event)
2986 unless $self->editor->create_action_transit_copy($transit);
2989 # ensure the transit is returned to the caller
2990 $self->transit($transit);
2992 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2994 $self->checkin_changed(1);
2998 sub hold_capture_is_possible {
3000 my $copy = $self->copy;
3002 # we've been explicitly told not to capture any holds
3003 return 0 if $self->capture eq 'nocapture';
3005 # See if this copy can fulfill any holds
3006 my $hold = $holdcode->find_nearest_permitted_hold(
3007 $self->editor, $copy, $self->editor->requestor, 1 # check_only
3009 return undef if ref $hold eq "HASH" and
3010 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
3014 sub reservation_capture_is_possible {
3016 my $copy = $self->copy;
3018 # we've been explicitly told not to capture any holds
3019 return 0 if $self->capture eq 'nocapture';
3021 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3022 my $resv = $booking_ses->request(
3023 "open-ils.booking.reservations.could_capture",
3024 $self->editor->authtoken, $copy->barcode
3026 $booking_ses->disconnect;
3027 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
3028 $self->push_events($resv);
3034 # returns true if the item was used (or may potentially be used
3035 # in subsequent calls) to capture a hold.
3036 sub attempt_checkin_hold_capture {
3038 my $copy = $self->copy;
3040 # we've been explicitly told not to capture any holds
3041 return 0 if $self->capture eq 'nocapture';
3043 # See if this copy can fulfill any holds
3044 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3045 $self->editor, $copy, $self->editor->requestor );
3048 $logger->debug("circulator: no potential permitted".
3049 "holds found for copy ".$copy->barcode);
3053 if($self->capture ne 'capture') {
3054 # see if this item is in a hold-capture-delay location
3055 my $location = $self->copy->location;
3056 if(!ref($location)) {
3057 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3058 $self->copy->location($location);
3060 if($U->is_true($location->hold_verify)) {
3061 $self->bail_on_events(
3062 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3067 $self->retarget($retarget);
3069 my $suppress_transit = 0;
3070 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3071 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3072 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3073 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3074 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3075 $suppress_transit = 1;
3076 $hold->pickup_lib($self->circ_lib);
3081 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3083 $hold->current_copy($copy->id);
3084 $hold->capture_time('now');
3085 $self->put_hold_on_shelf($hold)
3086 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3088 # prevent DB errors caused by fetching
3089 # holds from storage, and updating through cstore
3090 $hold->clear_fulfillment_time;
3091 $hold->clear_fulfillment_staff;
3092 $hold->clear_fulfillment_lib;
3093 $hold->clear_expire_time;
3094 $hold->clear_cancel_time;
3095 $hold->clear_prev_check_time unless $hold->prev_check_time;
3097 $self->bail_on_events($self->editor->event)
3098 unless $self->editor->update_action_hold_request($hold);
3100 $self->checkin_changed(1);
3102 return 0 if $self->bail_out;
3104 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3106 if ($hold->hold_type eq 'R') {
3107 $copy->status(OILS_COPY_STATUS_CATALOGING);
3108 $hold->fulfillment_time('now');
3109 $self->noop(1); # Block other transit/hold checks
3110 $self->bail_on_events($self->editor->event)
3111 unless $self->editor->update_action_hold_request($hold);
3113 # This hold was captured in the correct location
3114 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3115 $self->push_events(OpenILS::Event->new('SUCCESS'));
3117 #$self->do_hold_notify($hold->id);
3118 $self->notify_hold($hold->id);
3123 # Hold needs to be picked up elsewhere. Build a hold
3124 # transit and route the item.
3125 $self->checkin_build_hold_transit();
3126 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3127 return 0 if $self->bail_out;
3128 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3131 # make sure we save the copy status
3133 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3137 sub attempt_checkin_reservation_capture {
3139 my $copy = $self->copy;
3141 # we've been explicitly told not to capture any holds
3142 return 0 if $self->capture eq 'nocapture';
3144 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3145 my $evt = $booking_ses->request(
3146 "open-ils.booking.resources.capture_for_reservation",
3147 $self->editor->authtoken,
3149 1 # don't update copy - we probably have it locked
3151 $booking_ses->disconnect;
3153 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3155 "open-ils.booking.resources.capture_for_reservation " .
3156 "didn't return an event!"
3160 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3161 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3163 # not-transferable is an error event we'll pass on the user
3164 $logger->warn("reservation capture attempted against non-transferable item");
3165 $self->push_events($evt);
3167 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3168 # Re-retrieve copy as reservation capture may have changed
3169 # its status and whatnot.
3171 "circulator: booking capture win on copy " . $self->copy->id
3173 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3175 "circulator: changing copy " . $self->copy->id .
3176 "'s status from " . $self->copy->status . " to " .
3179 $self->copy->status($new_copy_status);
3182 $self->reservation($evt->{"payload"}->{"reservation"});
3184 if (exists $evt->{"payload"}->{"transit"}) {
3188 "org" => $evt->{"payload"}->{"transit"}->dest
3192 $self->checkin_changed(1);
3196 # other results are treated as "nothing to capture"
3200 sub do_hold_notify {
3201 my( $self, $holdid ) = @_;
3203 my $e = new_editor(xact => 1);
3204 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3206 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3207 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3209 $logger->info("circulator: running delayed hold notify process");
3211 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3212 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3214 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3215 hold_id => $holdid, requestor => $self->editor->requestor);
3217 $logger->debug("circulator: built hold notifier");
3219 if(!$notifier->event) {
3221 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3223 my $stat = $notifier->send_email_notify;
3224 if( $stat == '1' ) {
3225 $logger->info("circulator: hold notify succeeded for hold $holdid");
3229 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3232 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3236 sub retarget_holds {
3238 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3239 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3240 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3241 # no reason to wait for the return value
3245 sub checkin_build_hold_transit {
3248 my $copy = $self->copy;
3249 my $hold = $self->hold;
3250 my $trans = Fieldmapper::action::hold_transit_copy->new;
3252 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3254 $trans->hold($hold->id);
3255 $trans->source($self->circ_lib);
3256 $trans->dest($hold->pickup_lib);
3257 $trans->source_send_time("now");
3258 $trans->target_copy($copy->id);
3260 # when the copy gets to its destination, it will recover
3261 # this status - put it onto the holds shelf
3262 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3264 return $self->bail_on_events($self->editor->event)
3265 unless $self->editor->create_action_hold_transit_copy($trans);
3270 sub process_received_transit {
3272 my $copy = $self->copy;
3273 my $copyid = $self->copy->id;
3275 my $status_name = $U->copy_status($copy->status)->name;
3276 $logger->debug("circulator: attempting transit receive on ".
3277 "copy $copyid. Copy status is $status_name");
3279 my $transit = $self->transit;
3281 # Check if we are in a transit suppress range
3282 my $suppress_transit = 0;
3283 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3284 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3285 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3286 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3287 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3288 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3289 $suppress_transit = 1;
3290 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3294 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3295 # - this item is in-transit to a different location
3296 # - Or we are capturing holds as transits, so why create a new transit?
3298 my $tid = $transit->id;
3299 my $loc = $self->circ_lib;
3300 my $dest = $transit->dest;
3302 $logger->info("circulator: Fowarding transit on copy which is destined ".
3303 "for a different location. transit=$tid, copy=$copyid, current ".
3304 "location=$loc, destination location=$dest");
3306 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3308 # grab the associated hold object if available
3309 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3310 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3312 return $self->bail_on_events($evt);
3315 # The transit is received, set the receive time
3316 $transit->dest_recv_time('now');
3317 $self->bail_on_events($self->editor->event)
3318 unless $self->editor->update_action_transit_copy($transit);
3320 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3322 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3323 $copy->status( $transit->copy_status );
3324 $self->update_copy();
3325 return if $self->bail_out;
3329 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3332 # hold has arrived at destination, set shelf time
3333 $self->put_hold_on_shelf($hold);
3334 $self->bail_on_events($self->editor->event)
3335 unless $self->editor->update_action_hold_request($hold);
3336 return if $self->bail_out;
3338 $self->notify_hold($hold_transit->hold);
3341 $hold_transit = undef;
3342 $self->cancelled_hold_transit(1);
3343 $self->reshelve_copy(1);
3344 $self->fake_hold_dest(0);
3349 OpenILS::Event->new(
3352 payload => { transit => $transit, holdtransit => $hold_transit } ));
3354 return $hold_transit;
3358 # ------------------------------------------------------------------
3359 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3360 # ------------------------------------------------------------------
3361 sub put_hold_on_shelf {
3362 my($self, $hold) = @_;
3363 $hold->shelf_time('now');
3364 $hold->current_shelf_lib($self->circ_lib);
3365 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3371 my $reservation = shift;
3372 my $dt_parser = DateTime::Format::ISO8601->new;
3374 my $obj = $reservation ? $self->reservation : $self->circ;
3376 my $lost_bill_opts = $self->lost_bill_options;
3377 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3378 # first, restore any voided overdues for lost, if needed
3379 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3380 my $restore_od = $U->ou_ancestor_setting_value(
3381 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3382 $self->editor) || 0;
3383 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3387 # next, handle normal overdue generation and apply stop_fines
3388 # XXX reservations don't have stop_fines
3389 # TODO revisit booking_reservation re: stop_fines support
3390 if ($reservation or !$obj->stop_fines) {
3393 # This is a crude check for whether we are in a grace period. The code
3394 # in generate_fines() does a more thorough job, so this exists solely
3395 # as a small optimization, and might be better off removed.
3397 # If we have a grace period
3398 if($obj->can('grace_period')) {
3399 # Parse out the due date
3400 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3401 # Add the grace period to the due date
3402 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3403 # Don't generate fines on circs still in grace period
3404 $skip_for_grace = $due_date > DateTime->now;
3406 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3407 unless $skip_for_grace;
3409 if (!$reservation and !$obj->stop_fines) {
3410 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3411 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3412 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3413 $obj->stop_fines_time('now');
3414 $obj->stop_fines_time($self->backdate) if $self->backdate;
3415 $self->editor->update_action_circulation($obj);
3419 # finally, handle voiding of lost item and processing fees
3420 if ($self->needs_lost_bill_handling) {
3421 my $void_cost = $U->ou_ancestor_setting_value(
3422 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3423 $self->editor) || 0;
3424 my $void_proc_fee = $U->ou_ancestor_setting_value(
3425 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3426 $self->editor) || 0;
3427 $self->checkin_handle_lost_or_lo_now_found(
3428 $lost_bill_opts->{void_cost_btype},
3429 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3430 $self->checkin_handle_lost_or_lo_now_found(
3431 $lost_bill_opts->{void_fee_btype},
3432 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3438 sub checkin_handle_circ {
3440 my $circ = $self->circ;
3441 my $copy = $self->copy;
3445 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3447 # backdate the circ if necessary
3448 if($self->backdate) {
3449 my $evt = $self->checkin_handle_backdate;
3450 return $self->bail_on_events($evt) if $evt;
3453 # Set the checkin vars since we have the item
3454 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3456 # capture the true scan time for back-dated checkins
3457 $circ->checkin_scan_time('now');
3459 $circ->checkin_staff($self->editor->requestor->id);
3460 $circ->checkin_lib($self->circ_lib);
3461 $circ->checkin_workstation($self->editor->requestor->wsid);
3463 my $circ_lib = (ref $self->copy->circ_lib) ?
3464 $self->copy->circ_lib->id : $self->copy->circ_lib;
3465 my $stat = $U->copy_status($self->copy->status)->id;
3467 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3468 # we will now handle lost fines, but the copy will retain its 'lost'
3469 # status if it needs to transit home unless lost_immediately_available
3472 # if we decide to also delay fine handling until the item arrives home,
3473 # we will need to call lost fine handling code both when checking items
3474 # in and also when receiving transits
3475 $self->checkin_handle_lost($circ_lib);
3476 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3477 # same process as above.
3478 $self->checkin_handle_long_overdue($circ_lib);
3479 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3480 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3482 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3487 # see if there are any fines owed on this circ. if not, close it
3488 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3489 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3491 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3493 return $self->bail_on_events($self->editor->event)
3494 unless $self->editor->update_action_circulation($circ);
3499 # ------------------------------------------------------------------
3500 # See if we need to void billings, etc. for lost checkin
3501 # ------------------------------------------------------------------
3502 sub checkin_handle_lost {
3504 my $circ_lib = shift;
3506 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3507 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3509 $self->lost_bill_options({
3510 circ_lib => $circ_lib,
3511 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3512 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3513 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3514 void_cost_btype => 3,
3518 return $self->checkin_handle_lost_or_longoverdue(
3519 circ_lib => $circ_lib,
3520 max_return => $max_return,
3521 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3522 ous_use_last_activity => undef # not supported for LOST checkin
3526 # ------------------------------------------------------------------
3527 # See if we need to void billings, etc. for long-overdue checkin
3528 # note: not using constants below since they serve little purpose
3529 # for single-use strings that are descriptive in their own right
3530 # and mostly just complicate debugging.
3531 # ------------------------------------------------------------------
3532 sub checkin_handle_long_overdue {
3534 my $circ_lib = shift;
3536 $logger->info("circulator: processing long-overdue checkin...");
3538 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3539 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3541 $self->lost_bill_options({
3542 circ_lib => $circ_lib,
3543 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3544 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3545 is_longoverdue => 1,
3546 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3547 void_cost_btype => 10,
3548 void_fee_btype => 11
3551 return $self->checkin_handle_lost_or_longoverdue(
3552 circ_lib => $circ_lib,
3553 max_return => $max_return,
3554 ous_immediately_available => 'circ.longoverdue_immediately_available',
3555 ous_use_last_activity =>
3556 'circ.longoverdue.use_last_activity_date_on_return'
3560 # last billing activity is last payment time, last billing time, or the
3561 # circ due date. If the relevant "use last activity" org unit setting is
3562 # false/unset, then last billing activity is always the due date.
3563 sub get_circ_last_billing_activity {
3565 my $circ_lib = shift;
3566 my $setting = shift;
3567 my $date = $self->circ->due_date;
3569 return $date unless $setting and
3570 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3572 my $xact = $self->editor->retrieve_money_billable_transaction([
3574 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3577 if ($xact->summary) {
3578 $date = $xact->summary->last_payment_ts ||
3579 $xact->summary->last_billing_ts ||
3580 $self->circ->due_date;
3587 sub checkin_handle_lost_or_longoverdue {
3588 my ($self, %args) = @_;
3590 my $circ = $self->circ;
3591 my $max_return = $args{max_return};
3592 my $circ_lib = $args{circ_lib};
3597 $self->get_circ_last_billing_activity(
3598 $circ_lib, $args{ous_use_last_activity});
3601 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3602 $tm[5] -= 1 if $tm[5] > 0;
3603 my $due = timelocal(int($tm[1]), int($tm[2]),
3604 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3607 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3609 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3610 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3611 "DUE: $due LAST: $last_chance");
3613 $max_return = 0 if $today < $last_chance;
3619 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3620 "return interval. skipping fine/fee voiding, etc.");
3622 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3624 $logger->info("circulator: check-in of lost/lo item having a balance ".
3625 "of zero, skipping fine/fee voiding and reinstatement.");
3627 } else { # within max-return interval or no interval defined
3629 $logger->info("circulator: check-in of lost/lo item is within the ".
3630 "max return interval (or no interval is defined). Proceeding ".
3631 "with fine/fee voiding, etc.");
3633 $self->needs_lost_bill_handling(1);
3636 if ($circ_lib != $self->circ_lib) {
3637 # if the item is not home, check to see if we want to retain the
3638 # lost/longoverdue status at this point in the process
3640 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3641 $args{ous_immediately_available}, $self->editor) || 0;
3643 if ($immediately_available) {
3644 # item status does not need to be retained, so give it a
3645 # reshelving status as if it were a normal checkin
3646 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3649 $logger->info("circulator: leaving lost/longoverdue copy".
3650 " status in place on checkin");
3653 # lost/longoverdue item is home and processed, treat like a normal
3654 # checkin from this point on
3655 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3661 sub checkin_handle_backdate {
3664 # ------------------------------------------------------------------
3665 # clean up the backdate for date comparison
3666 # XXX We are currently taking the due-time from the original due-date,
3667 # not the input. Do we need to do this? This certainly interferes with
3668 # backdating of hourly checkouts, but that is likely a very rare case.
3669 # ------------------------------------------------------------------
3670 my $bd = cleanse_ISO8601($self->backdate);
3671 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3672 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3673 $new_date->set_hour($original_date->hour());
3674 $new_date->set_minute($original_date->minute());
3675 if ($new_date >= DateTime->now) {
3676 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3679 $bd = cleanse_ISO8601($new_date->datetime());
3682 $self->backdate($bd);
3687 sub check_checkin_copy_status {
3689 my $copy = $self->copy;
3691 my $status = $U->copy_status($copy->status)->id;
3694 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3695 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3696 $status == OILS_COPY_STATUS_IN_PROCESS ||
3697 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3698 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3699 $status == OILS_COPY_STATUS_CATALOGING ||
3700 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3701 $status == OILS_COPY_STATUS_RESHELVING );
3703 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3704 if( $status == OILS_COPY_STATUS_LOST );
3706 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3707 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3709 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3710 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3712 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3713 if( $status == OILS_COPY_STATUS_MISSING );
3715 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3720 # --------------------------------------------------------------------------
3721 # On checkin, we need to return as many relevant objects as we can
3722 # --------------------------------------------------------------------------
3723 sub checkin_flesh_events {
3726 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3727 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3728 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3731 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3734 if($self->hold and !$self->hold->cancel_time) {
3735 $hold = $self->hold;
3736 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3740 # update our copy of the circ object and
3741 # flesh the billing summary data
3743 $self->editor->retrieve_action_circulation([
3747 circ => ['billable_transaction'],
3756 # flesh some patron fields before returning
3758 $self->editor->retrieve_actor_user([
3763 au => ['card', 'billing_address', 'mailing_address']
3770 for my $evt (@{$self->events}) {
3773 $payload->{copy} = $U->unflesh_copy($self->copy);
3774 $payload->{volume} = $self->volume;
3775 $payload->{record} = $record,
3776 $payload->{circ} = $self->circ;
3777 $payload->{transit} = $self->transit;
3778 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3779 $payload->{hold} = $hold;
3780 $payload->{patron} = $self->patron;
3781 $payload->{reservation} = $self->reservation
3782 unless (not $self->reservation or $self->reservation->cancel_time);
3784 $evt->{payload} = $payload;
3789 my( $self, $msg ) = @_;
3790 my $bc = ($self->copy) ? $self->copy->barcode :
3793 my $usr = ($self->patron) ? $self->patron->id : "";
3794 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3795 ", recipient=$usr, copy=$bc");
3801 $self->log_me("do_renew()");
3803 # Make sure there is an open circ to renew
3804 my $usrid = $self->patron->id if $self->patron;
3805 my $circ = $self->editor->search_action_circulation({
3806 target_copy => $self->copy->id,
3807 xact_finish => undef,
3808 checkin_time => undef,
3809 ($usrid ? (usr => $usrid) : ())
3812 return $self->bail_on_events($self->editor->event) unless $circ;
3814 # A user is not allowed to renew another user's items without permission
3815 unless( $circ->usr eq $self->editor->requestor->id ) {
3816 return $self->bail_on_events($self->editor->events)
3817 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3820 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3821 if $circ->renewal_remaining < 1;
3823 # -----------------------------------------------------------------
3825 $self->parent_circ($circ->id);
3826 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3829 # Opac renewal - re-use circ library from original circ (unless told not to)
3830 if($self->opac_renewal) {
3831 unless(defined($opac_renewal_use_circ_lib)) {
3832 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3833 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3834 $opac_renewal_use_circ_lib = 1;
3837 $opac_renewal_use_circ_lib = 0;
3840 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3843 # Desk renewal - re-use circ library from original circ (unless told not to)
3844 if($self->desk_renewal) {
3845 unless(defined($desk_renewal_use_circ_lib)) {
3846 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3847 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3848 $desk_renewal_use_circ_lib = 1;
3851 $desk_renewal_use_circ_lib = 0;
3854 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3857 # Run the fine generator against the old circ
3858 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3859 # a few lines down. Commenting out, for now.
3860 #$self->handle_fines;
3862 $self->run_renew_permit;
3865 $self->do_checkin();
3866 return if $self->bail_out;
3868 unless( $self->permit_override ) {
3870 return if $self->bail_out;
3871 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3872 $self->remove_event('ITEM_NOT_CATALOGED');
3875 $self->override_events;
3876 return if $self->bail_out;
3879 $self->do_checkout();
3884 my( $self, $evt ) = @_;
3885 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3886 $logger->debug("circulator: removing event from list: $evt");
3887 my @events = @{$self->events};
3888 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3893 my( $self, $evt ) = @_;
3894 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3895 return grep { $_->{textcode} eq $evt } @{$self->events};
3900 sub run_renew_permit {
3903 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3904 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3905 $self->editor, $self->copy, $self->editor->requestor, 1
3907 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3910 if(!$self->legacy_script_support) {
3911 my $results = $self->run_indb_circ_test;
3912 $self->push_events($self->matrix_test_result_events)
3913 unless $self->circ_test_success;
3916 my $runner = $self->script_runner;
3918 $runner->load($self->circ_permit_renew);
3919 my $result = $runner->run or
3920 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3921 if ($result->{"events"}) {
3923 map { new OpenILS::Event($_) } @{$result->{"events"}}
3926 "circulator: circ_permit_renew for user " .
3927 $self->patron->id . " returned " .
3928 scalar(@{$result->{"events"}}) . " event(s)"
3932 $self->mk_script_runner;
3935 $logger->debug("circulator: re-creating script runner to be safe");
3939 # XXX: The primary mechanism for storing circ history is now handled
3940 # by tracking real circulation objects instead of bibs in a bucket.
3941 # However, this code is disabled by default and could be useful
3942 # some day, so may as well leave it for now.
3943 sub append_reading_list {
3947 $self->is_checkout and
3953 # verify history is globally enabled and uses the bucket mechanism
3954 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3955 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3957 return undef unless $htype and $htype eq 'bucket';
3959 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3961 # verify the patron wants to retain the hisory
3962 my $setting = $e->search_actor_user_setting(
3963 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3965 unless($setting and $setting->value) {
3970 my $bkt = $e->search_container_copy_bucket(
3971 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3976 # find the next item position
3977 my $last_item = $e->search_container_copy_bucket_item(
3978 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3979 $pos = $last_item->pos + 1 if $last_item;
3982 # create the history bucket if necessary
3983 $bkt = Fieldmapper::container::copy_bucket->new;
3984 $bkt->owner($self->patron->id);
3986 $bkt->btype('circ_history');
3988 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3991 my $item = Fieldmapper::container::copy_bucket_item->new;
3993 $item->bucket($bkt->id);
3994 $item->target_copy($self->copy->id);
3997 $e->create_container_copy_bucket_item($item) or return $e->die_event;
4004 sub make_trigger_events {
4006 return unless $self->circ;
4007 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
4008 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
4009 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
4014 sub checkin_handle_lost_or_lo_now_found {
4015 my ($self, $bill_type, $is_longoverdue) = @_;
4017 # ------------------------------------------------------------------
4018 # remove charge from patron's account if lost item is returned
4019 # ------------------------------------------------------------------
4021 my $bills = $self->editor->search_money_billing(
4023 xact => $self->circ->id,
4028 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4030 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
4031 for my $bill (@$bills) {
4032 if( !$U->is_true($bill->voided) ) {
4033 $logger->info("$tag item returned - voiding bill ".$bill->id);
4035 $bill->void_time('now');
4036 $bill->voider($self->editor->requestor->id);
4037 my $note = ($bill->note) ? $bill->note . "\n" : '';
4038 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
4040 $self->bail_on_events($self->editor->event)
4041 unless $self->editor->update_money_billing($bill);
4046 sub checkin_handle_lost_or_lo_now_found_restore_od {
4048 my $circ_lib = shift;
4049 my $is_longoverdue = shift;
4050 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4052 # ------------------------------------------------------------------
4053 # restore those overdue charges voided when item was set to lost
4054 # ------------------------------------------------------------------
4056 my $ods = $self->editor->search_money_billing(
4058 xact => $self->circ->id,
4063 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4064 for my $bill (@$ods) {
4065 if( $U->is_true($bill->voided) ) {
4066 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4068 $bill->clear_void_time;
4069 $bill->voider($self->editor->requestor->id);
4070 my $note = ($bill->note) ? $bill->note . "\n" : '';
4071 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4073 $self->bail_on_events($self->editor->event)
4074 unless $self->editor->update_money_billing($bill);