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;
19 sub determine_booking_status {
20 unless (defined $booking_status) {
21 my $ses = create OpenSRF::AppSession("router");
22 $booking_status = grep {$_ eq "open-ils.booking"} @{
23 $ses->request("opensrf.router.info.class.list")->gather(1)
26 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
29 return $booking_status;
35 flesh_fields => {acp => ['call_number','parts'], acn => ['record']}
41 my $conf = OpenSRF::Utils::SettingsClient->new;
42 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
44 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
45 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
47 my $lb = $conf->config_value( @pfx2, 'script_path' );
48 $lb = [ $lb ] unless ref($lb);
51 return unless $legacy_script_support;
53 my @pfx = ( @pfx2, "scripts" );
54 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
55 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
56 my $d = $conf->config_value( @pfx, 'circ_duration' );
57 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
58 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
59 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
61 $logger->error( "Missing circ script(s)" )
62 unless( $p and $c and $d and $f and $m and $pr );
64 $scripts{circ_permit_patron} = $p;
65 $scripts{circ_permit_copy} = $c;
66 $scripts{circ_duration} = $d;
67 $scripts{circ_recurring_fines} = $f;
68 $scripts{circ_max_fines} = $m;
69 $scripts{circ_permit_renew} = $pr;
72 "circulator: Loaded rules scripts for circ: " .
73 "circ permit patron = $p, ".
74 "circ permit copy = $c, ".
75 "circ duration = $d, ".
76 "circ recurring fines = $f, " .
77 "circ max fines = $m, ".
78 "circ renew permit = $pr. ".
80 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
84 __PACKAGE__->register_method(
85 method => "run_method",
86 api_name => "open-ils.circ.checkout.permit",
88 Determines if the given checkout can occur
89 @param authtoken The login session key
90 @param params A trailing hash of named params including
91 barcode : The copy barcode,
92 patron : The patron the checkout is occurring for,
93 renew : true or false - whether or not this is a renewal
94 @return The event that occurred during the permit check.
98 __PACKAGE__->register_method (
99 method => 'run_method',
100 api_name => 'open-ils.circ.checkout.permit.override',
101 signature => q/@see open-ils.circ.checkout.permit/,
105 __PACKAGE__->register_method(
106 method => "run_method",
107 api_name => "open-ils.circ.checkout",
110 @param authtoken The login session key
111 @param params A named hash of params including:
113 barcode If no copy is provided, the copy is retrieved via barcode
114 copyid If no copy or barcode is provide, the copy id will be use
115 patron The patron's id
116 noncat True if this is a circulation for a non-cataloted item
117 noncat_type The non-cataloged type id
118 noncat_circ_lib The location for the noncat circ.
119 precat The item has yet to be cataloged
120 dummy_title The temporary title of the pre-cataloded item
121 dummy_author The temporary authr of the pre-cataloded item
122 Default is the home org of the staff member
123 @return The SUCCESS event on success, any other event depending on the error
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.checkin",
131 Generic super-method for handling all copies
132 @param authtoken The login session key
133 @param params Hash of named parameters including:
134 barcode - The copy barcode
135 force - If true, copies in bad statuses will be checked in and give good statuses
136 noop - don't capture holds or put items into transit
137 void_overdues - void all overdues for the circulation (aka amnesty)
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkin.override",
145 signature => q/@see open-ils.circ.checkin/
148 __PACKAGE__->register_method(
149 method => "run_method",
150 api_name => "open-ils.circ.renew.override",
151 signature => q/@see open-ils.circ.renew/,
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.renew",
158 notes => <<" NOTES");
159 PARAMS( authtoken, circ => circ_id );
160 open-ils.circ.renew(login_session, circ_object);
161 Renews the provided circulation. login_session is the requestor of the
162 renewal and if the logged in user is not the same as circ->usr, then
163 the logged in user must have RENEW_CIRC permissions.
166 __PACKAGE__->register_method(
167 method => "run_method",
168 api_name => "open-ils.circ.checkout.full"
170 __PACKAGE__->register_method(
171 method => "run_method",
172 api_name => "open-ils.circ.checkout.full.override"
174 __PACKAGE__->register_method(
175 method => "run_method",
176 api_name => "open-ils.circ.reservation.pickup"
178 __PACKAGE__->register_method(
179 method => "run_method",
180 api_name => "open-ils.circ.reservation.return"
182 __PACKAGE__->register_method(
183 method => "run_method",
184 api_name => "open-ils.circ.reservation.return.override"
186 __PACKAGE__->register_method(
187 method => "run_method",
188 api_name => "open-ils.circ.checkout.inspect",
189 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
194 my( $self, $conn, $auth, $args ) = @_;
195 translate_legacy_args($args);
196 my $api = $self->api_name;
199 OpenILS::Application::Circ::Circulator->new($auth, %$args);
201 return circ_events($circulator) if $circulator->bail_out;
203 $circulator->use_booking(determine_booking_status());
205 # --------------------------------------------------------------------------
206 # First, check for a booking transit, as the barcode may not be a copy
207 # barcode, but a resource barcode, and nothing else in here will work
208 # --------------------------------------------------------------------------
210 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
211 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
212 if (@$resources) { # yes!
214 my $res_id_list = [ map { $_->id } @$resources ];
215 my $transit = $circulator->editor->search_action_reservation_transit_copy(
217 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
218 { order_by => { artc => 'source_send_time' }, limit => 1 }
220 )->[0]; # Any transit for this barcode?
222 if ($transit) { # yes! unwrap it.
224 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
225 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
227 my $success_event = new OpenILS::Event(
228 "SUCCESS", "payload" => {"reservation" => $reservation}
230 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
231 if (my $copy = $circulator->editor->search_asset_copy([
232 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
233 ])->[0]) { # got a copy
234 $copy->status( $transit->copy_status );
235 $copy->editor($circulator->editor->requestor->id);
236 $copy->edit_date('now');
237 $circulator->editor->update_asset_copy($copy);
238 $success_event->{"payload"}->{"record"} =
239 $U->record_to_mvr($copy->call_number->record);
240 $success_event->{"payload"}->{"volume"} = $copy->call_number;
241 $copy->call_number($copy->call_number->id);
242 $success_event->{"payload"}->{"copy"} = $copy;
246 $transit->dest_recv_time('now');
247 $circulator->editor->update_action_reservation_transit_copy( $transit );
249 $circulator->editor->commit;
250 # Formerly this branch just stopped here. Argh!
251 $conn->respond_complete($success_event);
259 # --------------------------------------------------------------------------
260 # Go ahead and load the script runner to make sure we have all
261 # of the objects we need
262 # --------------------------------------------------------------------------
264 if ($circulator->use_booking) {
265 $circulator->is_res_checkin($circulator->is_checkin(1))
266 if $api =~ /reservation.return/ or (
267 $api =~ /checkin/ and $circulator->seems_like_reservation()
270 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
273 $circulator->is_renewal(1) if $api =~ /renew/;
274 $circulator->is_checkin(1) if $api =~ /checkin/;
276 $circulator->mk_env();
277 $circulator->noop(1) if $circulator->claims_never_checked_out;
279 if($legacy_script_support and not $circulator->is_checkin) {
280 $circulator->mk_script_runner();
281 $circulator->legacy_script_support(1);
282 $circulator->circ_permit_patron($scripts{circ_permit_patron});
283 $circulator->circ_permit_copy($scripts{circ_permit_copy});
284 $circulator->circ_duration($scripts{circ_duration});
285 $circulator->circ_permit_renew($scripts{circ_permit_renew});
287 return circ_events($circulator) if $circulator->bail_out;
290 $circulator->override(1) if $api =~ /override/o;
292 if( $api =~ /checkout\.permit/ ) {
293 $circulator->do_permit();
295 } elsif( $api =~ /checkout.full/ ) {
297 # requesting a precat checkout implies that any required
298 # overrides have been performed. Go ahead and re-override.
299 $circulator->skip_permit_key(1);
300 $circulator->override(1) if $circulator->request_precat;
301 $circulator->do_permit();
302 $circulator->is_checkout(1);
303 unless( $circulator->bail_out ) {
304 $circulator->events([]);
305 $circulator->do_checkout();
308 } elsif( $circulator->is_res_checkout ) {
309 $circulator->do_reservation_pickup();
311 } elsif( $api =~ /inspect/ ) {
312 my $data = $circulator->do_inspect();
313 $circulator->editor->rollback;
316 } elsif( $api =~ /checkout/ ) {
317 $circulator->is_checkout(1);
318 $circulator->do_checkout();
320 } elsif( $circulator->is_res_checkin ) {
321 $circulator->do_reservation_return();
322 $circulator->do_checkin() if ($circulator->copy());
323 } elsif( $api =~ /checkin/ ) {
324 $circulator->do_checkin();
326 } elsif( $api =~ /renew/ ) {
327 $circulator->is_renewal(1);
328 $circulator->do_renew();
331 if( $circulator->bail_out ) {
334 # make sure no success event accidentally slip in
336 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
339 my @e = @{$circulator->events};
340 push( @ee, $_->{textcode} ) for @e;
341 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
343 $circulator->editor->rollback;
347 $circulator->editor->commit;
349 if ($circulator->generate_lost_overdue) {
350 # Generating additional overdue billings has to happen after the
351 # main commit and before the final respond() so the caller can
352 # receive the latest transaction summary.
353 my $evt = $circulator->generate_lost_overdue_fines;
354 $circulator->bail_on_events($evt) if $evt;
358 $conn->respond_complete(circ_events($circulator));
360 $circulator->script_runner->cleanup if $circulator->script_runner;
362 return undef if $circulator->bail_out;
364 $circulator->do_hold_notify($circulator->notify_hold)
365 if $circulator->notify_hold;
366 $circulator->retarget_holds if $circulator->retarget;
367 $circulator->append_reading_list;
368 $circulator->make_trigger_events;
375 my @e = @{$circ->events};
376 # if we have multiple events, SUCCESS should not be one of them;
377 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
378 return (@e == 1) ? $e[0] : \@e;
382 sub translate_legacy_args {
385 if( $$args{barcode} ) {
386 $$args{copy_barcode} = $$args{barcode};
387 delete $$args{barcode};
390 if( $$args{copyid} ) {
391 $$args{copy_id} = $$args{copyid};
392 delete $$args{copyid};
395 if( $$args{patronid} ) {
396 $$args{patron_id} = $$args{patronid};
397 delete $$args{patronid};
400 if( $$args{patron} and !ref($$args{patron}) ) {
401 $$args{patron_id} = $$args{patron};
402 delete $$args{patron};
406 if( $$args{noncat} ) {
407 $$args{is_noncat} = $$args{noncat};
408 delete $$args{noncat};
411 if( $$args{precat} ) {
412 $$args{is_precat} = $$args{request_precat} = $$args{precat};
413 delete $$args{precat};
419 # --------------------------------------------------------------------------
420 # This package actually manages all of the circulation logic
421 # --------------------------------------------------------------------------
422 package OpenILS::Application::Circ::Circulator;
423 use strict; use warnings;
424 use vars q/$AUTOLOAD/;
426 use OpenILS::Utils::Fieldmapper;
427 use OpenSRF::Utils::Cache;
428 use Digest::MD5 qw(md5_hex);
429 use DateTime::Format::ISO8601;
430 use OpenILS::Utils::PermitHold;
431 use OpenSRF::Utils qw/:datetime/;
432 use OpenSRF::Utils::SettingsClient;
433 use OpenILS::Application::Circ::Holds;
434 use OpenILS::Application::Circ::Transit;
435 use OpenSRF::Utils::Logger qw(:logger);
436 use OpenILS::Utils::CStoreEditor qw/:funcs/;
437 use OpenILS::Application::Circ::ScriptBuilder;
438 use OpenILS::Const qw/:const/;
439 use OpenILS::Utils::Penalty;
440 use OpenILS::Application::Circ::CircCommon;
443 my $holdcode = "OpenILS::Application::Circ::Holds";
444 my $transcode = "OpenILS::Application::Circ::Transit";
450 # --------------------------------------------------------------------------
451 # Add a pile of automagic getter/setter methods
452 # --------------------------------------------------------------------------
453 my @AUTOLOAD_FIELDS = qw/
500 recurring_fines_level
513 cancelled_hold_transit
520 circ_matrix_matchpoint
522 legacy_script_support
532 claims_never_checked_out
537 generate_lost_overdue
544 my $type = ref($self) or die "$self is not an object";
546 my $name = $AUTOLOAD;
549 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
550 $logger->error("circulator: $type: invalid autoload field: $name");
551 die "$type: invalid autoload field: $name\n"
556 *{"${type}::${name}"} = sub {
559 $s->{$name} = $v if defined $v;
563 return $self->$name($data);
568 my( $class, $auth, %args ) = @_;
569 $class = ref($class) || $class;
570 my $self = bless( {}, $class );
573 $self->editor(new_editor(xact => 1, authtoken => $auth));
575 unless( $self->editor->checkauth ) {
576 $self->bail_on_events($self->editor->event);
580 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
582 $self->$_($args{$_}) for keys %args;
585 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
587 # if this is a renewal, default to desk_renewal
588 $self->desk_renewal(1) unless
589 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
591 $self->capture('') unless $self->capture;
593 unless(%user_groups) {
594 my $gps = $self->editor->retrieve_all_permission_grp_tree;
595 %user_groups = map { $_->id => $_ } @$gps;
602 # --------------------------------------------------------------------------
603 # True if we should discontinue processing
604 # --------------------------------------------------------------------------
606 my( $self, $bool ) = @_;
607 if( defined $bool ) {
608 $logger->info("circulator: BAILING OUT") if $bool;
609 $self->{bail_out} = $bool;
611 return $self->{bail_out};
616 my( $self, @evts ) = @_;
619 $e->{payload} = $self->copy if
620 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
622 $logger->info("circulator: pushing event ".$e->{textcode});
623 push( @{$self->events}, $e ) unless
624 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
630 return '' if $self->skip_permit_key;
631 my $key = md5_hex( time() . rand() . "$$" );
632 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
633 return $self->permit_key($key);
636 sub check_permit_key {
638 return 1 if $self->skip_permit_key;
639 my $key = $self->permit_key;
640 return 0 unless $key;
641 my $k = "oils_permit_key_$key";
642 my $one = $self->cache_handle->get_cache($k);
643 $self->cache_handle->delete_cache($k);
644 return ($one) ? 1 : 0;
647 sub seems_like_reservation {
650 # Some words about the following method:
651 # 1) It requires the VIEW_USER permission, but that's not an
652 # issue, right, since all staff should have that?
653 # 2) It returns only one reservation at a time, even if an item can be
654 # and is currently overbooked. Hmmm....
655 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
656 my $result = $booking_ses->request(
657 "open-ils.booking.reservations.by_returnable_resource_barcode",
658 $self->editor->authtoken,
661 $booking_ses->disconnect;
663 return $self->bail_on_events($result) if defined $U->event_code($result);
666 $self->reservation(shift @$result);
674 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
675 sub save_trimmed_copy {
676 my ($self, $copy) = @_;
679 $self->volume($copy->call_number);
680 $self->title($self->volume->record);
681 $self->copy->call_number($self->volume->id);
682 $self->volume->record($self->title->id);
683 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
684 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
685 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
686 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
692 my $e = $self->editor;
694 # --------------------------------------------------------------------------
695 # Grab the fleshed copy
696 # --------------------------------------------------------------------------
697 unless($self->is_noncat) {
700 $copy = $e->retrieve_asset_copy(
701 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
703 } elsif( $self->copy_barcode ) {
705 $copy = $e->search_asset_copy(
706 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
707 } elsif( $self->reservation ) {
708 my $res = $e->json_query(
710 "select" => {"acp" => ["id"]},
715 "field" => "barcode",
719 "field" => "current_resource"
727 "id" => (ref $self->reservation) ?
728 $self->reservation->id : $self->reservation
733 if (ref $res eq "ARRAY" and scalar @$res) {
734 $logger->info("circulator: mapped reservation " .
735 $self->reservation . " to copy " . $res->[0]->{"id"});
736 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
741 $self->save_trimmed_copy($copy);
743 # We can't renew if there is no copy
744 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
745 if $self->is_renewal;
750 # --------------------------------------------------------------------------
752 # --------------------------------------------------------------------------
756 flesh_fields => {au => [ qw/ card / ]}
759 if( $self->patron_id ) {
760 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
761 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
763 } elsif( $self->patron_barcode ) {
765 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
766 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
767 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
769 $patron = $e->retrieve_actor_user($card->usr)
770 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
772 # Use the card we looked up, not the patron's primary, for card active checks
773 $patron->card($card);
776 if( my $copy = $self->copy ) {
779 $flesh->{flesh_fields}->{circ} = ['usr'];
781 my $circ = $e->search_action_circulation([
782 {target_copy => $copy->id, checkin_time => undef}, $flesh
786 $patron = $circ->usr;
787 $circ->usr($patron->id); # de-flesh for consistency
793 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
794 unless $self->patron($patron) or $self->is_checkin;
796 unless($self->is_checkin) {
798 # Check for inactivity and patron reg. expiration
800 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
801 unless $U->is_true($patron->active);
803 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
804 unless $U->is_true($patron->card->active);
806 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
807 cleanse_ISO8601($patron->expire_date));
809 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
810 if( CORE::time > $expire->epoch ) ;
814 # --------------------------------------------------------------------------
815 # This builds the script runner environment and fetches most of the
817 # --------------------------------------------------------------------------
818 sub mk_script_runner {
824 qw/copy copy_barcode copy_id patron
825 patron_id patron_barcode volume title editor/;
827 # Translate our objects into the ScriptBuilder args hash
828 $$args{$_} = $self->$_() for @fields;
830 $args->{ignore_user_status} = 1 if $self->is_checkin;
831 $$args{fetch_patron_by_circ_copy} = 1;
832 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
834 if( my $pco = $self->pending_checkouts ) {
835 $logger->info("circulator: we were given a pending checkouts number of $pco");
836 $$args{patronItemsOut} = $pco;
839 # This fetches most of the objects we need
840 $self->script_runner(
841 OpenILS::Application::Circ::ScriptBuilder->build($args));
843 # Now we translate the ScriptBuilder objects back into self
844 $self->$_($$args{$_}) for @fields;
846 my @evts = @{$args->{_events}} if $args->{_events};
848 $logger->debug("circulator: script builder returned events: @evts") if @evts;
852 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
853 if(!$self->is_noncat and
855 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
859 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
860 return $self->bail_on_events(@e);
865 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
866 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
867 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
868 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
872 # We can't renew if there is no copy
873 return $self->bail_on_events(@evts) if
874 $self->is_renewal and !$self->copy;
876 # Set some circ-specific flags in the script environment
877 my $evt = "environment";
878 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
880 if( $self->is_noncat ) {
881 $self->script_runner->insert("$evt.isNonCat", 1);
882 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
885 if( $self->is_precat ) {
886 $self->script_runner->insert("environment.isPrecat", 1, 1);
889 $self->script_runner->add_path( $_ ) for @$script_libs;
894 # --------------------------------------------------------------------------
895 # Does the circ permit work
896 # --------------------------------------------------------------------------
900 $self->log_me("do_permit()");
902 unless( $self->editor->requestor->id == $self->patron->id ) {
903 return $self->bail_on_events($self->editor->event)
904 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
907 $self->check_captured_holds();
908 $self->do_copy_checks();
909 return if $self->bail_out;
910 $self->run_patron_permit_scripts();
911 $self->run_copy_permit_scripts()
912 unless $self->is_precat or $self->is_noncat;
913 $self->check_item_deposit_events();
914 $self->override_events();
915 return if $self->bail_out;
917 if($self->is_precat and not $self->request_precat) {
920 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
921 return $self->bail_out(1) unless $self->is_renewal;
925 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
928 sub check_item_deposit_events {
930 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
931 if $self->is_deposit and not $self->is_deposit_exempt;
932 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
933 if $self->is_rental and not $self->is_rental_exempt;
936 # returns true if the user is not required to pay deposits
937 sub is_deposit_exempt {
939 my $pid = (ref $self->patron->profile) ?
940 $self->patron->profile->id : $self->patron->profile;
941 my $groups = $U->ou_ancestor_setting_value(
942 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
943 for my $grp (@$groups) {
944 return 1 if $self->is_group_descendant($grp, $pid);
949 # returns true if the user is not required to pay rental fees
950 sub is_rental_exempt {
952 my $pid = (ref $self->patron->profile) ?
953 $self->patron->profile->id : $self->patron->profile;
954 my $groups = $U->ou_ancestor_setting_value(
955 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
956 for my $grp (@$groups) {
957 return 1 if $self->is_group_descendant($grp, $pid);
962 sub is_group_descendant {
963 my($self, $p_id, $c_id) = @_;
964 return 0 unless defined $p_id and defined $c_id;
965 return 1 if $c_id == $p_id;
966 while(my $grp = $user_groups{$c_id}) {
967 $c_id = $grp->parent;
968 return 0 unless defined $c_id;
969 return 1 if $c_id == $p_id;
974 sub check_captured_holds {
976 my $copy = $self->copy;
977 my $patron = $self->patron;
979 return undef unless $copy;
981 my $s = $U->copy_status($copy->status)->id;
982 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
983 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
985 # Item is on the holds shelf, make sure it's going to the right person
986 my $holds = $self->editor->search_action_hold_request(
989 current_copy => $copy->id ,
990 capture_time => { '!=' => undef },
991 cancel_time => undef,
992 fulfillment_time => undef
998 if( $holds and $$holds[0] ) {
999 return undef if $$holds[0]->usr == $patron->id;
1002 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1004 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1008 sub do_copy_checks {
1010 my $copy = $self->copy;
1011 return unless $copy;
1013 my $stat = $U->copy_status($copy->status)->id;
1015 # We cannot check out a copy if it is in-transit
1016 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1017 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1020 $self->handle_claims_returned();
1021 return if $self->bail_out;
1023 # no claims returned circ was found, check if there is any open circ
1024 unless( $self->is_renewal ) {
1026 my $circs = $self->editor->search_action_circulation(
1027 { target_copy => $copy->id, checkin_time => undef }
1030 if(my $old_circ = $circs->[0]) { # an open circ was found
1032 my $payload = {copy => $copy};
1034 if($old_circ->usr == $self->patron->id) {
1036 $payload->{old_circ} = $old_circ;
1038 # If there is an open circulation on the checkout item and an auto-renew
1039 # interval is defined, inform the caller that they should go
1040 # ahead and renew the item instead of warning about open circulations.
1042 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1044 'circ.checkout_auto_renew_age',
1048 if($auto_renew_intvl) {
1049 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1050 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1052 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1053 $payload->{auto_renew} = 1;
1058 return $self->bail_on_events(
1059 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1065 my $LEGACY_CIRC_EVENT_MAP = {
1066 'no_item' => 'ITEM_NOT_CATALOGED',
1067 'actor.usr.barred' => 'PATRON_BARRED',
1068 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1069 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1070 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1071 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1072 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1073 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1074 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1075 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1079 # ---------------------------------------------------------------------
1080 # This pushes any patron-related events into the list but does not
1081 # set bail_out for any events
1082 # ---------------------------------------------------------------------
1083 sub run_patron_permit_scripts {
1085 my $runner = $self->script_runner;
1086 my $patronid = $self->patron->id;
1090 if(!$self->legacy_script_support) {
1092 my $results = $self->run_indb_circ_test;
1093 unless($self->circ_test_success) {
1094 # no_item result is OK during noncat checkout
1095 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1096 push @allevents, $self->matrix_test_result_events;
1102 # ---------------------------------------------------------------------
1103 # # Now run the patron permit script
1104 # ---------------------------------------------------------------------
1105 $runner->load($self->circ_permit_patron);
1106 my $result = $runner->run or
1107 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1109 my $patron_events = $result->{events};
1111 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1112 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1113 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1114 $penalties = $penalties->{fatal_penalties};
1116 for my $pen (@$penalties) {
1117 my $event = OpenILS::Event->new($pen->name);
1118 $event->{desc} = $pen->label;
1119 push(@allevents, $event);
1122 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1126 $_->{payload} = $self->copy if
1127 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1130 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1132 $self->push_events(@allevents);
1135 sub matrix_test_result_codes {
1137 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1140 sub matrix_test_result_events {
1143 my $event = new OpenILS::Event(
1144 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1146 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1148 } (@{$self->matrix_test_result});
1151 sub run_indb_circ_test {
1153 return $self->matrix_test_result if $self->matrix_test_result;
1155 my $dbfunc = ($self->is_renewal) ?
1156 'action.item_user_renew_test' : 'action.item_user_circ_test';
1158 if( $self->is_precat && $self->request_precat) {
1159 $self->make_precat_copy;
1160 return if $self->bail_out;
1163 my $results = $self->editor->json_query(
1167 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1173 $self->circ_test_success($U->is_true($results->[0]->{success}));
1175 if(my $mp = $results->[0]->{matchpoint}) {
1176 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1177 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1178 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1179 if(defined($results->[0]->{renewals})) {
1180 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1182 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1183 if(defined($results->[0]->{grace_period})) {
1184 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1186 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1187 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1190 return $self->matrix_test_result($results);
1193 # ---------------------------------------------------------------------
1194 # given a use and copy, this will calculate the circulation policy
1195 # parameters. Only works with in-db circ.
1196 # ---------------------------------------------------------------------
1200 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1202 $self->run_indb_circ_test;
1205 circ_test_success => $self->circ_test_success,
1206 failure_events => [],
1207 failure_codes => [],
1208 matchpoint => $self->circ_matrix_matchpoint
1211 unless($self->circ_test_success) {
1212 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1213 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1216 if($self->circ_matrix_matchpoint) {
1217 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1218 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1219 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1220 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1222 my $policy = $self->get_circ_policy(
1223 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1225 $$results{$_} = $$policy{$_} for keys %$policy;
1231 # ---------------------------------------------------------------------
1232 # Loads the circ policy info for duration, recurring fine, and max
1233 # fine based on the current copy
1234 # ---------------------------------------------------------------------
1235 sub get_circ_policy {
1236 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1239 duration_rule => $duration_rule->name,
1240 recurring_fine_rule => $recurring_fine_rule->name,
1241 max_fine_rule => $max_fine_rule->name,
1242 max_fine => $self->get_max_fine_amount($max_fine_rule),
1243 fine_interval => $recurring_fine_rule->recurrence_interval,
1244 renewal_remaining => $duration_rule->max_renewals,
1245 grace_period => $recurring_fine_rule->grace_period
1248 if($hard_due_date) {
1249 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1250 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1253 $policy->{duration_date_ceiling} = undef;
1254 $policy->{duration_date_ceiling_force} = undef;
1257 $policy->{duration} = $duration_rule->shrt
1258 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1259 $policy->{duration} = $duration_rule->normal
1260 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1261 $policy->{duration} = $duration_rule->extended
1262 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1264 $policy->{recurring_fine} = $recurring_fine_rule->low
1265 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1266 $policy->{recurring_fine} = $recurring_fine_rule->normal
1267 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1268 $policy->{recurring_fine} = $recurring_fine_rule->high
1269 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1274 sub get_max_fine_amount {
1276 my $max_fine_rule = shift;
1277 my $max_amount = $max_fine_rule->amount;
1279 # if is_percent is true then the max->amount is
1280 # use as a percentage of the copy price
1281 if ($U->is_true($max_fine_rule->is_percent)) {
1282 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1283 $max_amount = $price * $max_fine_rule->amount / 100;
1285 $U->ou_ancestor_setting_value(
1287 'circ.max_fine.cap_at_price',
1291 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1292 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1300 sub run_copy_permit_scripts {
1302 my $copy = $self->copy || return;
1303 my $runner = $self->script_runner;
1307 if(!$self->legacy_script_support) {
1308 my $results = $self->run_indb_circ_test;
1309 push @allevents, $self->matrix_test_result_events
1310 unless $self->circ_test_success;
1313 # ---------------------------------------------------------------------
1314 # Capture all of the copy permit events
1315 # ---------------------------------------------------------------------
1316 $runner->load($self->circ_permit_copy);
1317 my $result = $runner->run or
1318 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1319 my $copy_events = $result->{events};
1321 # ---------------------------------------------------------------------
1322 # Now collect all of the events together
1323 # ---------------------------------------------------------------------
1324 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1327 # See if this copy has an alert message
1328 my $ae = $self->check_copy_alert();
1329 push( @allevents, $ae ) if $ae;
1331 # uniquify the events
1332 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1333 @allevents = values %hash;
1335 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1337 $self->push_events(@allevents);
1341 sub check_copy_alert {
1343 return undef if $self->is_renewal;
1344 return OpenILS::Event->new(
1345 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1346 if $self->copy and $self->copy->alert_message;
1352 # --------------------------------------------------------------------------
1353 # If the call is overriding and has permissions to override every collected
1354 # event, the are cleared. Any event that the caller does not have
1355 # permission to override, will be left in the event list and bail_out will
1357 # XXX We need code in here to cancel any holds/transits on copies
1358 # that are being force-checked out
1359 # --------------------------------------------------------------------------
1360 sub override_events {
1362 my @events = @{$self->events};
1363 return unless @events;
1365 if(!$self->override) {
1366 return $self->bail_out(1)
1367 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1372 for my $e (@events) {
1373 my $tc = $e->{textcode};
1374 next if $tc eq 'SUCCESS';
1375 my $ov = "$tc.override";
1376 $logger->info("circulator: attempting to override event: $ov");
1378 return $self->bail_on_events($self->editor->event)
1379 unless( $self->editor->allowed($ov) );
1384 # --------------------------------------------------------------------------
1385 # If there is an open claimsreturn circ on the requested copy, close the
1386 # circ if overriding, otherwise bail out
1387 # --------------------------------------------------------------------------
1388 sub handle_claims_returned {
1390 my $copy = $self->copy;
1392 my $CR = $self->editor->search_action_circulation(
1394 target_copy => $copy->id,
1395 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1396 checkin_time => undef,
1400 return unless ($CR = $CR->[0]);
1404 # - If the caller has set the override flag, we will check the item in
1405 if($self->override) {
1407 $CR->checkin_time('now');
1408 $CR->checkin_scan_time('now');
1409 $CR->checkin_lib($self->circ_lib);
1410 $CR->checkin_workstation($self->editor->requestor->wsid);
1411 $CR->checkin_staff($self->editor->requestor->id);
1413 $evt = $self->editor->event
1414 unless $self->editor->update_action_circulation($CR);
1417 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1420 $self->bail_on_events($evt) if $evt;
1425 # --------------------------------------------------------------------------
1426 # This performs the checkout
1427 # --------------------------------------------------------------------------
1431 $self->log_me("do_checkout()");
1433 # make sure perms are good if this isn't a renewal
1434 unless( $self->is_renewal ) {
1435 return $self->bail_on_events($self->editor->event)
1436 unless( $self->editor->allowed('COPY_CHECKOUT') );
1439 # verify the permit key
1440 unless( $self->check_permit_key ) {
1441 if( $self->permit_override ) {
1442 return $self->bail_on_events($self->editor->event)
1443 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1445 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1449 # if this is a non-cataloged circ, build the circ and finish
1450 if( $self->is_noncat ) {
1451 $self->checkout_noncat;
1453 OpenILS::Event->new('SUCCESS',
1454 payload => { noncat_circ => $self->circ }));
1458 if( $self->is_precat ) {
1459 $self->make_precat_copy;
1460 return if $self->bail_out;
1462 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1463 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1466 $self->do_copy_checks;
1467 return if $self->bail_out;
1469 $self->run_checkout_scripts();
1470 return if $self->bail_out;
1472 $self->build_checkout_circ_object();
1473 return if $self->bail_out;
1475 my $modify_to_start = $self->booking_adjusted_due_date();
1476 return if $self->bail_out;
1478 $self->apply_modified_due_date($modify_to_start);
1479 return if $self->bail_out;
1481 return $self->bail_on_events($self->editor->event)
1482 unless $self->editor->create_action_circulation($self->circ);
1484 # refresh the circ to force local time zone for now
1485 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1487 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1489 return if $self->bail_out;
1491 $self->apply_deposit_fee();
1492 return if $self->bail_out;
1494 $self->handle_checkout_holds();
1495 return if $self->bail_out;
1497 # ------------------------------------------------------------------------------
1498 # Update the patron penalty info in the DB. Run it for permit-overrides
1499 # since the penalties are not updated during the permit phase
1500 # ------------------------------------------------------------------------------
1501 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1503 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1506 if($self->is_renewal) {
1507 # flesh the billing summary for the checked-in circ
1508 $pcirc = $self->editor->retrieve_action_circulation([
1510 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1515 OpenILS::Event->new('SUCCESS',
1517 copy => $U->unflesh_copy($self->copy),
1518 volume => $self->volume,
1519 circ => $self->circ,
1521 holds_fulfilled => $self->fulfilled_holds,
1522 deposit_billing => $self->deposit_billing,
1523 rental_billing => $self->rental_billing,
1524 parent_circ => $pcirc,
1525 patron => ($self->return_patron) ? $self->patron : undef,
1526 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1532 sub apply_deposit_fee {
1534 my $copy = $self->copy;
1536 ($self->is_deposit and not $self->is_deposit_exempt) or
1537 ($self->is_rental and not $self->is_rental_exempt);
1539 return if $self->is_deposit and $self->skip_deposit_fee;
1540 return if $self->is_rental and $self->skip_rental_fee;
1542 my $bill = Fieldmapper::money::billing->new;
1543 my $amount = $copy->deposit_amount;
1547 if($self->is_deposit) {
1548 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1550 $self->deposit_billing($bill);
1552 $billing_type = OILS_BILLING_TYPE_RENTAL;
1554 $self->rental_billing($bill);
1557 $bill->xact($self->circ->id);
1558 $bill->amount($amount);
1559 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1560 $bill->billing_type($billing_type);
1561 $bill->btype($btype);
1562 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1564 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1569 my $copy = $self->copy;
1571 my $stat = $copy->status if ref $copy->status;
1572 my $loc = $copy->location if ref $copy->location;
1573 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1575 $copy->status($stat->id) if $stat;
1576 $copy->location($loc->id) if $loc;
1577 $copy->circ_lib($circ_lib->id) if $circ_lib;
1578 $copy->editor($self->editor->requestor->id);
1579 $copy->edit_date('now');
1580 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1582 return $self->bail_on_events($self->editor->event)
1583 unless $self->editor->update_asset_copy($self->copy);
1585 $copy->status($U->copy_status($copy->status));
1586 $copy->location($loc) if $loc;
1587 $copy->circ_lib($circ_lib) if $circ_lib;
1590 sub update_reservation {
1592 my $reservation = $self->reservation;
1594 my $usr = $reservation->usr;
1595 my $target_rt = $reservation->target_resource_type;
1596 my $target_r = $reservation->target_resource;
1597 my $current_r = $reservation->current_resource;
1599 $reservation->usr($usr->id) if ref $usr;
1600 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1601 $reservation->target_resource($target_r->id) if ref $target_r;
1602 $reservation->current_resource($current_r->id) if ref $current_r;
1604 return $self->bail_on_events($self->editor->event)
1605 unless $self->editor->update_booking_reservation($self->reservation);
1608 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1609 $self->reservation($reservation);
1613 sub bail_on_events {
1614 my( $self, @evts ) = @_;
1615 $self->push_events(@evts);
1620 # ------------------------------------------------------------------------------
1621 # When an item is checked out, see if we can fulfill a hold for this patron
1622 # ------------------------------------------------------------------------------
1623 sub handle_checkout_holds {
1625 my $copy = $self->copy;
1626 my $patron = $self->patron;
1628 my $e = $self->editor;
1629 $self->fulfilled_holds([]);
1631 # pre/non-cats can't fulfill a hold
1632 return if $self->is_precat or $self->is_noncat;
1634 my $hold = $e->search_action_hold_request({
1635 current_copy => $copy->id ,
1636 cancel_time => undef,
1637 fulfillment_time => undef,
1639 {expire_time => undef},
1640 {expire_time => {'>' => 'now'}}
1644 if($hold and $hold->usr != $patron->id) {
1645 # reset the hold since the copy is now checked out
1647 $logger->info("circulator: un-targeting hold ".$hold->id.
1648 " because copy ".$copy->id." is getting checked out");
1650 $hold->clear_prev_check_time;
1651 $hold->clear_current_copy;
1652 $hold->clear_capture_time;
1654 return $self->bail_on_event($e->event)
1655 unless $e->update_action_hold_request($hold);
1661 $hold = $self->find_related_user_hold($copy, $patron) or return;
1662 $logger->info("circulator: found related hold to fulfill in checkout");
1665 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1667 # if the hold was never officially captured, capture it.
1668 $hold->current_copy($copy->id);
1669 $hold->capture_time('now') unless $hold->capture_time;
1670 $hold->fulfillment_time('now');
1671 $hold->fulfillment_staff($e->requestor->id);
1672 $hold->fulfillment_lib($self->circ_lib);
1674 return $self->bail_on_events($e->event)
1675 unless $e->update_action_hold_request($hold);
1677 $holdcode->delete_hold_copy_maps($e, $hold->id);
1678 return $self->fulfilled_holds([$hold->id]);
1682 # ------------------------------------------------------------------------------
1683 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1684 # the patron directly targets the checked out item, see if there is another hold
1685 # for the patron that could be fulfilled by the checked out item. Fulfill the
1686 # oldest hold and only fulfill 1 of them.
1688 # For "another hold":
1690 # First, check for one that the copy matches via hold_copy_map, ensuring that
1691 # *any* hold type that this copy could fill may end up filled.
1693 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1694 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1695 # that are non-requestable to count as capturing those hold types.
1696 # ------------------------------------------------------------------------------
1697 sub find_related_user_hold {
1698 my($self, $copy, $patron) = @_;
1699 my $e = $self->editor;
1701 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1703 return undef unless $U->ou_ancestor_setting_value(
1704 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1706 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1708 select => {ahr => ['id']},
1720 fulfillment_time => undef,
1721 cancel_time => undef,
1723 {expire_time => undef},
1724 {expire_time => {'>' => 'now'}}
1728 target_copy => $self->copy->id
1731 order_by => {ahr => {request_time => {direction => 'asc'}}},
1735 my $hold_info = $e->json_query($args)->[0];
1736 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1737 return undef if $U->ou_ancestor_setting_value(
1738 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1740 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1742 select => {ahr => ['id']},
1747 fkey => 'current_copy',
1748 type => 'left' # there may be no current_copy
1755 fulfillment_time => undef,
1756 cancel_time => undef,
1758 {expire_time => undef},
1759 {expire_time => {'>' => 'now'}}
1766 target => $self->volume->id
1772 target => $self->title->id
1778 {id => undef}, # left-join copy may be nonexistent
1779 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1783 order_by => {ahr => {request_time => {direction => 'asc'}}},
1787 $hold_info = $e->json_query($args)->[0];
1788 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1793 sub run_checkout_scripts {
1798 my $runner = $self->script_runner;
1807 my $hard_due_date_name;
1809 if(!$self->legacy_script_support) {
1810 $self->run_indb_circ_test();
1811 $duration = $self->circ_matrix_matchpoint->duration_rule;
1812 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1813 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1814 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1818 $runner->load($self->circ_duration);
1820 my $result = $runner->run or
1821 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1823 $duration_name = $result->{durationRule};
1824 $recurring_name = $result->{recurringFinesRule};
1825 $max_fine_name = $result->{maxFine};
1826 $hard_due_date_name = $result->{hardDueDate};
1829 $duration_name = $duration->name if $duration;
1830 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1833 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1834 return $self->bail_on_events($evt) if ($evt && !$nobail);
1836 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1837 return $self->bail_on_events($evt) if ($evt && !$nobail);
1839 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1840 return $self->bail_on_events($evt) if ($evt && !$nobail);
1842 if($hard_due_date_name) {
1843 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1844 return $self->bail_on_events($evt) if ($evt && !$nobail);
1850 # The item circulates with an unlimited duration
1854 $hard_due_date = undef;
1857 $self->duration_rule($duration);
1858 $self->recurring_fines_rule($recurring);
1859 $self->max_fine_rule($max_fine);
1860 $self->hard_due_date($hard_due_date);
1864 sub build_checkout_circ_object {
1867 my $circ = Fieldmapper::action::circulation->new;
1868 my $duration = $self->duration_rule;
1869 my $max = $self->max_fine_rule;
1870 my $recurring = $self->recurring_fines_rule;
1871 my $hard_due_date = $self->hard_due_date;
1872 my $copy = $self->copy;
1873 my $patron = $self->patron;
1874 my $duration_date_ceiling;
1875 my $duration_date_ceiling_force;
1879 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1880 $duration_date_ceiling = $policy->{duration_date_ceiling};
1881 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1883 my $dname = $duration->name;
1884 my $mname = $max->name;
1885 my $rname = $recurring->name;
1887 if($hard_due_date) {
1888 $hdname = $hard_due_date->name;
1891 $logger->debug("circulator: building circulation ".
1892 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1894 $circ->duration($policy->{duration});
1895 $circ->recurring_fine($policy->{recurring_fine});
1896 $circ->duration_rule($duration->name);
1897 $circ->recurring_fine_rule($recurring->name);
1898 $circ->max_fine_rule($max->name);
1899 $circ->max_fine($policy->{max_fine});
1900 $circ->fine_interval($recurring->recurrence_interval);
1901 $circ->renewal_remaining($duration->max_renewals);
1902 $circ->grace_period($policy->{grace_period});
1906 $logger->info("circulator: copy found with an unlimited circ duration");
1907 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1908 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1909 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1910 $circ->renewal_remaining(0);
1911 $circ->grace_period(0);
1914 $circ->target_copy( $copy->id );
1915 $circ->usr( $patron->id );
1916 $circ->circ_lib( $self->circ_lib );
1917 $circ->workstation($self->editor->requestor->wsid)
1918 if defined $self->editor->requestor->wsid;
1920 # renewals maintain a link to the parent circulation
1921 $circ->parent_circ($self->parent_circ);
1923 if( $self->is_renewal ) {
1924 $circ->opac_renewal('t') if $self->opac_renewal;
1925 $circ->phone_renewal('t') if $self->phone_renewal;
1926 $circ->desk_renewal('t') if $self->desk_renewal;
1927 $circ->renewal_remaining($self->renewal_remaining);
1928 $circ->circ_staff($self->editor->requestor->id);
1932 # if the user provided an overiding checkout time,
1933 # (e.g. the checkout really happened several hours ago), then
1934 # we apply that here. Does this need a perm??
1935 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1936 if $self->checkout_time;
1938 # if a patron is renewing, 'requestor' will be the patron
1939 $circ->circ_staff($self->editor->requestor->id);
1940 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1945 sub do_reservation_pickup {
1948 $self->log_me("do_reservation_pickup()");
1950 $self->reservation->pickup_time('now');
1953 $self->reservation->current_resource &&
1954 $U->is_true($self->reservation->target_resource_type->catalog_item)
1956 # We used to try to set $self->copy and $self->patron here,
1957 # but that should already be done.
1959 $self->run_checkout_scripts(1);
1961 my $duration = $self->duration_rule;
1962 my $max = $self->max_fine_rule;
1963 my $recurring = $self->recurring_fines_rule;
1965 if ($duration && $max && $recurring) {
1966 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1968 my $dname = $duration->name;
1969 my $mname = $max->name;
1970 my $rname = $recurring->name;
1972 $logger->debug("circulator: updating reservation ".
1973 "with duration=$dname, maxfine=$mname, recurring=$rname");
1975 $self->reservation->fine_amount($policy->{recurring_fine});
1976 $self->reservation->max_fine($policy->{max_fine});
1977 $self->reservation->fine_interval($recurring->recurrence_interval);
1980 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1981 $self->update_copy();
1984 $self->reservation->fine_amount(
1985 $self->reservation->target_resource_type->fine_amount
1987 $self->reservation->max_fine(
1988 $self->reservation->target_resource_type->max_fine
1990 $self->reservation->fine_interval(
1991 $self->reservation->target_resource_type->fine_interval
1995 $self->update_reservation();
1998 sub do_reservation_return {
2000 my $request = shift;
2002 $self->log_me("do_reservation_return()");
2004 if (not ref $self->reservation) {
2005 my ($reservation, $evt) =
2006 $U->fetch_booking_reservation($self->reservation);
2007 return $self->bail_on_events($evt) if $evt;
2008 $self->reservation($reservation);
2011 $self->generate_fines(1);
2012 $self->reservation->return_time('now');
2013 $self->update_reservation();
2014 $self->reshelve_copy if $self->copy;
2016 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2017 $self->copy( $self->reservation->current_resource->catalog_item );
2021 sub booking_adjusted_due_date {
2023 my $circ = $self->circ;
2024 my $copy = $self->copy;
2026 return undef unless $self->use_booking;
2030 if( $self->due_date ) {
2032 return $self->bail_on_events($self->editor->event)
2033 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2035 $circ->due_date(cleanse_ISO8601($self->due_date));
2039 return unless $copy and $circ->due_date;
2042 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2043 if (@$booking_items) {
2044 my $booking_item = $booking_items->[0];
2045 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2047 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2048 my $shorten_circ_setting = $resource_type->elbow_room ||
2049 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2052 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2053 my $bookings = $booking_ses->request(
2054 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2055 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
2057 $booking_ses->disconnect;
2059 my $dt_parser = DateTime::Format::ISO8601->new;
2060 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2062 for my $bid (@$bookings) {
2064 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2066 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2067 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2069 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2070 if ($booking_start < DateTime->now);
2073 if ($U->is_true($stop_circ_setting)) {
2074 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2076 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2077 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2080 # We set the circ duration here only to affect the logic that will
2081 # later (in a DB trigger) mangle the time part of the due date to
2082 # 11:59pm. Having any circ duration that is not a whole number of
2083 # days is enough to prevent the "correction."
2084 my $new_circ_duration = $due_date->epoch - time;
2085 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2086 $circ->duration("$new_circ_duration seconds");
2088 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2092 return $self->bail_on_events($self->editor->event)
2093 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2099 sub apply_modified_due_date {
2101 my $shift_earlier = shift;
2102 my $circ = $self->circ;
2103 my $copy = $self->copy;
2105 if( $self->due_date ) {
2107 return $self->bail_on_events($self->editor->event)
2108 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2110 $circ->due_date(cleanse_ISO8601($self->due_date));
2114 # if the due_date lands on a day when the location is closed
2115 return unless $copy and $circ->due_date;
2117 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2119 # due-date overlap should be determined by the location the item
2120 # is checked out from, not the owning or circ lib of the item
2121 my $org = $self->circ_lib;
2123 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2124 " with an item due date of ".$circ->due_date );
2126 my $dateinfo = $U->storagereq(
2127 'open-ils.storage.actor.org_unit.closed_date.overlap',
2128 $org, $circ->due_date );
2131 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2132 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2134 # XXX make the behavior more dynamic
2135 # for now, we just push the due date to after the close date
2136 if ($shift_earlier) {
2137 $circ->due_date($dateinfo->{start});
2139 $circ->due_date($dateinfo->{end});
2147 sub create_due_date {
2148 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2150 # if there is a raw time component (e.g. from postgres),
2151 # turn it into an interval that interval_to_seconds can parse
2152 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2154 # for now, use the server timezone. TODO: use workstation org timezone
2155 my $due_date = DateTime->now(time_zone => 'local');
2157 # add the circ duration
2158 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2161 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2162 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2163 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2168 # return ISO8601 time with timezone
2169 return $due_date->strftime('%FT%T%z');
2174 sub make_precat_copy {
2176 my $copy = $self->copy;
2179 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2181 $copy->editor($self->editor->requestor->id);
2182 $copy->edit_date('now');
2183 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2184 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2185 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2186 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2187 $self->update_copy();
2191 $logger->info("circulator: Creating a new precataloged ".
2192 "copy in checkout with barcode " . $self->copy_barcode);
2194 $copy = Fieldmapper::asset::copy->new;
2195 $copy->circ_lib($self->circ_lib);
2196 $copy->creator($self->editor->requestor->id);
2197 $copy->editor($self->editor->requestor->id);
2198 $copy->barcode($self->copy_barcode);
2199 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2200 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2201 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2203 $copy->dummy_title($self->dummy_title || "");
2204 $copy->dummy_author($self->dummy_author || "");
2205 $copy->dummy_isbn($self->dummy_isbn || "");
2206 $copy->circ_modifier($self->circ_modifier);
2209 # See if we need to override the circ_lib for the copy with a configured circ_lib
2210 # Setting is shortname of the org unit
2211 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2212 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2214 if($precat_circ_lib) {
2215 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2218 $self->bail_on_events($self->editor->event);
2222 $copy->circ_lib($org->id);
2226 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2228 $self->push_events($self->editor->event);
2232 # this is a little bit of a hack, but we need to
2233 # get the copy into the script runner
2234 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2238 sub checkout_noncat {
2244 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2245 my $count = $self->noncat_count || 1;
2246 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2248 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2252 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2253 $self->editor->requestor->id,
2261 $self->push_events($evt);
2269 # If a copy goes into transit and is then checked in before the transit checkin
2270 # interval has expired, push an event onto the overridable events list.
2271 sub check_transit_checkin_interval {
2274 # only concerned with in-transit items
2275 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2277 # no interval, no problem
2278 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2279 return unless $interval;
2281 # capture the transit so we don't have to fetch it again later during checkin
2283 $self->editor->search_action_transit_copy(
2284 {target_copy => $self->copy->id, dest_recv_time => undef}
2288 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2289 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2290 my $horizon = $t_start->add(seconds => $seconds);
2292 # See if we are still within the transit checkin forbidden range
2293 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2294 if $horizon > DateTime->now;
2300 $self->log_me("do_checkin()");
2302 return $self->bail_on_events(
2303 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2306 $self->check_transit_checkin_interval;
2308 # the renew code and mk_env should have already found our circulation object
2309 unless( $self->circ ) {
2311 my $circs = $self->editor->search_action_circulation(
2312 { target_copy => $self->copy->id, checkin_time => undef });
2314 $self->circ($$circs[0]);
2316 # for now, just warn if there are multiple open circs on a copy
2317 $logger->warn("circulator: we have ".scalar(@$circs).
2318 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2321 # run the fine generator against this circ, if this circ is there
2322 $self->generate_fines_start if $self->circ;
2324 if( $self->checkin_check_holds_shelf() ) {
2325 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2326 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2327 $self->checkin_flesh_events;
2331 unless( $self->is_renewal ) {
2332 return $self->bail_on_events($self->editor->event)
2333 unless $self->editor->allowed('COPY_CHECKIN');
2336 $self->push_events($self->check_copy_alert());
2337 $self->push_events($self->check_checkin_copy_status());
2339 # if the circ is marked as 'claims returned', add the event to the list
2340 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2341 if ($self->circ and $self->circ->stop_fines
2342 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2344 $self->check_circ_deposit();
2346 # handle the overridable events
2347 $self->override_events unless $self->is_renewal;
2348 return if $self->bail_out;
2350 if( $self->copy and !$self->transit ) {
2352 $self->editor->search_action_transit_copy(
2353 { target_copy => $self->copy->id, dest_recv_time => undef }
2359 $self->generate_fines_finish;
2360 $self->checkin_handle_circ;
2361 return if $self->bail_out;
2362 $self->checkin_changed(1);
2364 } elsif( $self->transit ) {
2365 my $hold_transit = $self->process_received_transit;
2366 $self->checkin_changed(1);
2368 if( $self->bail_out ) {
2369 $self->checkin_flesh_events;
2373 if( my $e = $self->check_checkin_copy_status() ) {
2374 # If the original copy status is special, alert the caller
2375 my $ev = $self->events;
2376 $self->events([$e]);
2377 $self->override_events;
2378 return if $self->bail_out;
2382 if( $hold_transit or
2383 $U->copy_status($self->copy->status)->id
2384 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2387 if( $hold_transit ) {
2388 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2390 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2395 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2397 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2398 $self->reshelve_copy(1);
2399 $self->cancelled_hold_transit(1);
2400 $self->notify_hold(0); # don't notify for cancelled holds
2401 return if $self->bail_out;
2405 # hold transited to correct location
2406 $self->checkin_flesh_events;
2411 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2413 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2414 " that is in-transit, but there is no transit.. repairing");
2415 $self->reshelve_copy(1);
2416 return if $self->bail_out;
2419 if( $self->is_renewal ) {
2420 $self->finish_fines_and_voiding;
2421 return if $self->bail_out;
2422 $self->push_events(OpenILS::Event->new('SUCCESS'));
2426 # ------------------------------------------------------------------------------
2427 # Circulations and transits are now closed where necessary. Now go on to see if
2428 # this copy can fulfill a hold or needs to be routed to a different location
2429 # ------------------------------------------------------------------------------
2431 my $needed_for_something = 0; # formerly "needed_for_hold"
2433 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2435 if (!$self->remote_hold) {
2436 if ($self->use_booking) {
2437 my $potential_hold = $self->hold_capture_is_possible;
2438 my $potential_reservation = $self->reservation_capture_is_possible;
2440 if ($potential_hold and $potential_reservation) {
2441 $logger->info("circulator: item could fulfill either hold or reservation");
2442 $self->push_events(new OpenILS::Event(
2443 "HOLD_RESERVATION_CONFLICT",
2444 "hold" => $potential_hold,
2445 "reservation" => $potential_reservation
2447 return if $self->bail_out;
2448 } elsif ($potential_hold) {
2449 $needed_for_something =
2450 $self->attempt_checkin_hold_capture;
2451 } elsif ($potential_reservation) {
2452 $needed_for_something =
2453 $self->attempt_checkin_reservation_capture;
2456 $needed_for_something = $self->attempt_checkin_hold_capture;
2459 return if $self->bail_out;
2461 unless($needed_for_something) {
2462 my $circ_lib = (ref $self->copy->circ_lib) ?
2463 $self->copy->circ_lib->id : $self->copy->circ_lib;
2465 if( $self->remote_hold ) {
2466 $circ_lib = $self->remote_hold->pickup_lib;
2467 $logger->warn("circulator: Copy ".$self->copy->barcode.
2468 " is on a remote hold's shelf, sending to $circ_lib");
2471 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2473 if( $circ_lib == $self->circ_lib) {
2474 # copy is where it needs to be, either for hold or reshelving
2476 $self->checkin_handle_precat();
2477 return if $self->bail_out;
2480 # copy needs to transit "home", or stick here if it's a floating copy
2482 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2483 $self->checkin_changed(1);
2484 $self->copy->circ_lib( $self->circ_lib );
2487 my $bc = $self->copy->barcode;
2488 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2489 $self->checkin_build_copy_transit($circ_lib);
2490 return if $self->bail_out;
2491 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2495 } else { # no-op checkin
2496 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2497 $self->checkin_changed(1);
2498 $self->copy->circ_lib( $self->circ_lib );
2503 if($self->claims_never_checked_out and
2504 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2506 # the item was not supposed to be checked out to the user and should now be marked as missing
2507 $self->copy->status(OILS_COPY_STATUS_MISSING);
2511 $self->reshelve_copy unless $needed_for_something;
2514 return if $self->bail_out;
2516 unless($self->checkin_changed) {
2518 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2519 my $stat = $U->copy_status($self->copy->status)->id;
2521 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2522 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2523 $self->bail_out(1); # no need to commit anything
2527 $self->push_events(OpenILS::Event->new('SUCCESS'))
2528 unless @{$self->events};
2531 $self->finish_fines_and_voiding;
2533 OpenILS::Utils::Penalty->calculate_penalties(
2534 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2536 $self->checkin_flesh_events;
2540 sub finish_fines_and_voiding {
2542 return unless $self->circ;
2544 # gather any updates to the circ after fine generation, if there was a circ
2545 $self->generate_fines_finish;
2547 return unless $self->backdate or $self->void_overdues;
2549 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2550 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2552 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2553 $self->editor, $self->circ, $self->backdate, $note);
2555 return $self->bail_on_events($evt) if $evt;
2557 # make sure the circ isn't closed if we just voided some fines
2558 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2559 return $self->bail_on_events($evt) if $evt;
2565 # if a deposit was payed for this item, push the event
2566 sub check_circ_deposit {
2568 return unless $self->circ;
2569 my $deposit = $self->editor->search_money_billing(
2571 xact => $self->circ->id,
2573 }, {idlist => 1})->[0];
2575 $self->push_events(OpenILS::Event->new(
2576 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2581 my $force = $self->force || shift;
2582 my $copy = $self->copy;
2584 my $stat = $U->copy_status($copy->status)->id;
2587 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2588 $stat != OILS_COPY_STATUS_CATALOGING and
2589 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2590 $stat != OILS_COPY_STATUS_RESHELVING )) {
2592 $copy->status( OILS_COPY_STATUS_RESHELVING );
2594 $self->checkin_changed(1);
2599 # Returns true if the item is at the current location
2600 # because it was transited there for a hold and the
2601 # hold has not been fulfilled
2602 sub checkin_check_holds_shelf {
2604 return 0 unless $self->copy;
2607 $U->copy_status($self->copy->status)->id ==
2608 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2610 # Attempt to clear shelf expired holds for this copy
2611 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2612 if($self->clear_expired);
2614 # find the hold that put us on the holds shelf
2615 my $holds = $self->editor->search_action_hold_request(
2617 current_copy => $self->copy->id,
2618 capture_time => { '!=' => undef },
2619 fulfillment_time => undef,
2620 cancel_time => undef,
2625 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2626 $self->reshelve_copy(1);
2630 my $hold = $$holds[0];
2632 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2633 $hold->id. "] for copy ".$self->copy->barcode);
2635 if( $hold->pickup_lib == $self->circ_lib ) {
2636 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2640 $logger->info("circulator: hold is not for here..");
2641 $self->remote_hold($hold);
2646 sub checkin_handle_precat {
2648 my $copy = $self->copy;
2650 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2651 $copy->status(OILS_COPY_STATUS_CATALOGING);
2652 $self->update_copy();
2653 $self->checkin_changed(1);
2654 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2659 sub checkin_build_copy_transit {
2662 my $copy = $self->copy;
2663 my $transit = Fieldmapper::action::transit_copy->new;
2665 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2666 $logger->info("circulator: transiting copy to $dest");
2668 $transit->source($self->circ_lib);
2669 $transit->dest($dest);
2670 $transit->target_copy($copy->id);
2671 $transit->source_send_time('now');
2672 $transit->copy_status( $U->copy_status($copy->status)->id );
2674 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2676 return $self->bail_on_events($self->editor->event)
2677 unless $self->editor->create_action_transit_copy($transit);
2679 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2681 $self->checkin_changed(1);
2685 sub hold_capture_is_possible {
2687 my $copy = $self->copy;
2689 # we've been explicitly told not to capture any holds
2690 return 0 if $self->capture eq 'nocapture';
2692 # See if this copy can fulfill any holds
2693 my $hold = $holdcode->find_nearest_permitted_hold(
2694 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2696 return undef if ref $hold eq "HASH" and
2697 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2701 sub reservation_capture_is_possible {
2703 my $copy = $self->copy;
2705 # we've been explicitly told not to capture any holds
2706 return 0 if $self->capture eq 'nocapture';
2708 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2709 my $resv = $booking_ses->request(
2710 "open-ils.booking.reservations.could_capture",
2711 $self->editor->authtoken, $copy->barcode
2713 $booking_ses->disconnect;
2714 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2715 $self->push_events($resv);
2721 # returns true if the item was used (or may potentially be used
2722 # in subsequent calls) to capture a hold.
2723 sub attempt_checkin_hold_capture {
2725 my $copy = $self->copy;
2727 # we've been explicitly told not to capture any holds
2728 return 0 if $self->capture eq 'nocapture';
2730 # See if this copy can fulfill any holds
2731 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2732 $self->editor, $copy, $self->editor->requestor );
2735 $logger->debug("circulator: no potential permitted".
2736 "holds found for copy ".$copy->barcode);
2740 if($self->capture ne 'capture') {
2741 # see if this item is in a hold-capture-delay location
2742 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2743 if($U->is_true($location->hold_verify)) {
2744 $self->bail_on_events(
2745 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2750 $self->retarget($retarget);
2752 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2754 $hold->current_copy($copy->id);
2755 $hold->capture_time('now');
2756 $self->put_hold_on_shelf($hold)
2757 if $hold->pickup_lib == $self->circ_lib;
2759 # prevent DB errors caused by fetching
2760 # holds from storage, and updating through cstore
2761 $hold->clear_fulfillment_time;
2762 $hold->clear_fulfillment_staff;
2763 $hold->clear_fulfillment_lib;
2764 $hold->clear_expire_time;
2765 $hold->clear_cancel_time;
2766 $hold->clear_prev_check_time unless $hold->prev_check_time;
2768 $self->bail_on_events($self->editor->event)
2769 unless $self->editor->update_action_hold_request($hold);
2771 $self->checkin_changed(1);
2773 return 0 if $self->bail_out;
2775 if( $hold->pickup_lib == $self->circ_lib ) {
2777 # This hold was captured in the correct location
2778 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2779 $self->push_events(OpenILS::Event->new('SUCCESS'));
2781 #$self->do_hold_notify($hold->id);
2782 $self->notify_hold($hold->id);
2786 # Hold needs to be picked up elsewhere. Build a hold
2787 # transit and route the item.
2788 $self->checkin_build_hold_transit();
2789 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2790 return 0 if $self->bail_out;
2791 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2794 # make sure we save the copy status
2799 sub attempt_checkin_reservation_capture {
2801 my $copy = $self->copy;
2803 # we've been explicitly told not to capture any holds
2804 return 0 if $self->capture eq 'nocapture';
2806 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2807 my $evt = $booking_ses->request(
2808 "open-ils.booking.resources.capture_for_reservation",
2809 $self->editor->authtoken,
2811 1 # don't update copy - we probably have it locked
2813 $booking_ses->disconnect;
2815 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2817 "open-ils.booking.resources.capture_for_reservation " .
2818 "didn't return an event!"
2822 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2823 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2825 # not-transferable is an error event we'll pass on the user
2826 $logger->warn("reservation capture attempted against non-transferable item");
2827 $self->push_events($evt);
2829 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2830 # Re-retrieve copy as reservation capture may have changed
2831 # its status and whatnot.
2833 "circulator: booking capture win on copy " . $self->copy->id
2835 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2837 "circulator: changing copy " . $self->copy->id .
2838 "'s status from " . $self->copy->status . " to " .
2841 $self->copy->status($new_copy_status);
2844 $self->reservation($evt->{"payload"}->{"reservation"});
2846 if (exists $evt->{"payload"}->{"transit"}) {
2850 "org" => $evt->{"payload"}->{"transit"}->dest
2854 $self->checkin_changed(1);
2858 # other results are treated as "nothing to capture"
2862 sub do_hold_notify {
2863 my( $self, $holdid ) = @_;
2865 my $e = new_editor(xact => 1);
2866 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2868 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2869 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2871 $logger->info("circulator: running delayed hold notify process");
2873 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2874 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2876 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2877 hold_id => $holdid, requestor => $self->editor->requestor);
2879 $logger->debug("circulator: built hold notifier");
2881 if(!$notifier->event) {
2883 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2885 my $stat = $notifier->send_email_notify;
2886 if( $stat == '1' ) {
2887 $logger->info("circulator: hold notify succeeded for hold $holdid");
2891 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2894 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2898 sub retarget_holds {
2900 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2901 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2902 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2903 # no reason to wait for the return value
2907 sub checkin_build_hold_transit {
2910 my $copy = $self->copy;
2911 my $hold = $self->hold;
2912 my $trans = Fieldmapper::action::hold_transit_copy->new;
2914 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2916 $trans->hold($hold->id);
2917 $trans->source($self->circ_lib);
2918 $trans->dest($hold->pickup_lib);
2919 $trans->source_send_time("now");
2920 $trans->target_copy($copy->id);
2922 # when the copy gets to its destination, it will recover
2923 # this status - put it onto the holds shelf
2924 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2926 return $self->bail_on_events($self->editor->event)
2927 unless $self->editor->create_action_hold_transit_copy($trans);
2932 sub process_received_transit {
2934 my $copy = $self->copy;
2935 my $copyid = $self->copy->id;
2937 my $status_name = $U->copy_status($copy->status)->name;
2938 $logger->debug("circulator: attempting transit receive on ".
2939 "copy $copyid. Copy status is $status_name");
2941 my $transit = $self->transit;
2943 if( $transit->dest != $self->circ_lib ) {
2944 # - this item is in-transit to a different location
2946 my $tid = $transit->id;
2947 my $loc = $self->circ_lib;
2948 my $dest = $transit->dest;
2950 $logger->info("circulator: Fowarding transit on copy which is destined ".
2951 "for a different location. transit=$tid, copy=$copyid, current ".
2952 "location=$loc, destination location=$dest");
2954 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2956 # grab the associated hold object if available
2957 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2958 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2960 return $self->bail_on_events($evt);
2963 # The transit is received, set the receive time
2964 $transit->dest_recv_time('now');
2965 $self->bail_on_events($self->editor->event)
2966 unless $self->editor->update_action_transit_copy($transit);
2968 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2970 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2971 $copy->status( $transit->copy_status );
2972 $self->update_copy();
2973 return if $self->bail_out;
2977 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2979 # hold has arrived at destination, set shelf time
2980 $self->put_hold_on_shelf($hold);
2981 $self->bail_on_events($self->editor->event)
2982 unless $self->editor->update_action_hold_request($hold);
2983 return if $self->bail_out;
2985 $self->notify_hold($hold_transit->hold);
2990 OpenILS::Event->new(
2993 payload => { transit => $transit, holdtransit => $hold_transit } ));
2995 return $hold_transit;
2999 # ------------------------------------------------------------------
3000 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3001 # ------------------------------------------------------------------
3002 sub put_hold_on_shelf {
3003 my($self, $hold) = @_;
3005 $hold->shelf_time('now');
3007 my $shelf_expire = $U->ou_ancestor_setting_value(
3008 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
3010 return undef unless $shelf_expire;
3012 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
3013 my $expire_time = DateTime->now->add(seconds => $seconds);
3015 # if the shelf expire time overlaps with a pickup lib's
3016 # closed date, push it out to the first open date
3017 my $dateinfo = $U->storagereq(
3018 'open-ils.storage.actor.org_unit.closed_date.overlap',
3019 $hold->pickup_lib, $expire_time);
3022 my $dt_parser = DateTime::Format::ISO8601->new;
3023 $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
3025 # TODO: enable/disable time bump via setting?
3026 $expire_time->set(hour => '23', minute => '59', second => '59');
3028 $logger->info("circulator: shelf_expire_time overlaps".
3029 " with closed date, pushing expire time to $expire_time");
3032 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
3038 sub generate_fines {
3040 my $reservation = shift;
3042 $self->generate_fines_start($reservation);
3043 $self->generate_fines_finish($reservation);
3048 sub generate_fines_start {
3050 my $reservation = shift;
3051 my $dt_parser = DateTime::Format::ISO8601->new;
3053 my $obj = $reservation ? $self->reservation : $self->circ;
3055 # If we have a grace period
3056 if($obj->can('grace_period')) {
3057 # Parse out the due date
3058 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3059 # Add the grace period to the due date
3060 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3061 # Don't generate fines on circs still in grace period
3062 return undef if ($due_date > DateTime->now);
3065 if (!exists($self->{_gen_fines_req})) {
3066 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3068 'open-ils.storage.action.circulation.overdue.generate_fines',
3076 sub generate_fines_finish {
3078 my $reservation = shift;
3080 return undef unless $self->{_gen_fines_req};
3082 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3084 $self->{_gen_fines_req}->wait_complete;
3085 delete($self->{_gen_fines_req});
3087 # refresh the circ in case the fine generator set the stop_fines field
3088 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3089 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3094 sub checkin_handle_circ {
3096 my $circ = $self->circ;
3097 my $copy = $self->copy;
3101 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3103 # backdate the circ if necessary
3104 if($self->backdate) {
3105 my $evt = $self->checkin_handle_backdate;
3106 return $self->bail_on_events($evt) if $evt;
3109 if(!$circ->stop_fines) {
3110 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3111 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3112 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3113 $circ->stop_fines_time('now');
3114 $circ->stop_fines_time($self->backdate) if $self->backdate;
3117 # Set the checkin vars since we have the item
3118 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3120 # capture the true scan time for back-dated checkins
3121 $circ->checkin_scan_time('now');
3123 $circ->checkin_staff($self->editor->requestor->id);
3124 $circ->checkin_lib($self->circ_lib);
3125 $circ->checkin_workstation($self->editor->requestor->wsid);
3127 my $circ_lib = (ref $self->copy->circ_lib) ?
3128 $self->copy->circ_lib->id : $self->copy->circ_lib;
3129 my $stat = $U->copy_status($self->copy->status)->id;
3131 # immediately available keeps items lost or missing items from going home before being handled
3132 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3133 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3136 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3138 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3139 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3141 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3145 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3147 $self->checkin_handle_lost($circ_lib);
3151 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3156 # see if there are any fines owed on this circ. if not, close it
3157 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3158 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3160 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3162 return $self->bail_on_events($self->editor->event)
3163 unless $self->editor->update_action_circulation($circ);
3169 # ------------------------------------------------------------------
3170 # See if we need to void billings for lost checkin
3171 # ------------------------------------------------------------------
3172 sub checkin_handle_lost {
3174 my $circ_lib = shift;
3175 my $circ = $self->circ;
3177 my $max_return = $U->ou_ancestor_setting_value(
3178 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3183 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3184 $tm[5] -= 1 if $tm[5] > 0;
3185 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3187 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3188 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3190 $max_return = 0 if $today < $last_chance;
3193 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3195 my $void_lost = $U->ou_ancestor_setting_value(
3196 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3197 my $void_lost_fee = $U->ou_ancestor_setting_value(
3198 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3199 my $restore_od = $U->ou_ancestor_setting_value(
3200 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3201 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3202 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3204 $self->checkin_handle_lost_now_found(3) if $void_lost;
3205 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3206 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3209 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3214 sub checkin_handle_backdate {
3217 # ------------------------------------------------------------------
3218 # clean up the backdate for date comparison
3219 # XXX We are currently taking the due-time from the original due-date,
3220 # not the input. Do we need to do this? This certainly interferes with
3221 # backdating of hourly checkouts, but that is likely a very rare case.
3222 # ------------------------------------------------------------------
3223 my $bd = cleanse_ISO8601($self->backdate);
3224 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3225 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3226 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3228 $self->backdate($bd);
3233 sub check_checkin_copy_status {
3235 my $copy = $self->copy;
3237 my $status = $U->copy_status($copy->status)->id;
3240 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3241 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3242 $status == OILS_COPY_STATUS_IN_PROCESS ||
3243 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3244 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3245 $status == OILS_COPY_STATUS_CATALOGING ||
3246 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3247 $status == OILS_COPY_STATUS_RESHELVING );
3249 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3250 if( $status == OILS_COPY_STATUS_LOST );
3252 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3253 if( $status == OILS_COPY_STATUS_MISSING );
3255 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3260 # --------------------------------------------------------------------------
3261 # On checkin, we need to return as many relevant objects as we can
3262 # --------------------------------------------------------------------------
3263 sub checkin_flesh_events {
3266 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3267 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3268 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3271 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3274 if($self->hold and !$self->hold->cancel_time) {
3275 $hold = $self->hold;
3276 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3280 # if we checked in a circulation, flesh the billing summary data
3281 $self->circ->billable_transaction(
3282 $self->editor->retrieve_money_billable_transaction([
3284 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3290 # flesh some patron fields before returning
3292 $self->editor->retrieve_actor_user([
3297 au => ['card', 'billing_address', 'mailing_address']
3304 for my $evt (@{$self->events}) {
3307 $payload->{copy} = $U->unflesh_copy($self->copy);
3308 $payload->{volume} = $self->volume;
3309 $payload->{record} = $record,
3310 $payload->{circ} = $self->circ;
3311 $payload->{transit} = $self->transit;
3312 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3313 $payload->{hold} = $hold;
3314 $payload->{patron} = $self->patron;
3315 $payload->{reservation} = $self->reservation
3316 unless (not $self->reservation or $self->reservation->cancel_time);
3318 $evt->{payload} = $payload;
3323 my( $self, $msg ) = @_;
3324 my $bc = ($self->copy) ? $self->copy->barcode :
3327 my $usr = ($self->patron) ? $self->patron->id : "";
3328 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3329 ", recipient=$usr, copy=$bc");
3335 $self->log_me("do_renew()");
3337 # Make sure there is an open circ to renew that is not
3338 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3339 my $usrid = $self->patron->id if $self->patron;
3340 my $circ = $self->editor->search_action_circulation({
3341 target_copy => $self->copy->id,
3342 xact_finish => undef,
3343 checkin_time => undef,
3344 ($usrid ? (usr => $usrid) : ()),
3346 {stop_fines => undef},
3347 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3351 return $self->bail_on_events($self->editor->event) unless $circ;
3353 # A user is not allowed to renew another user's items without permission
3354 unless( $circ->usr eq $self->editor->requestor->id ) {
3355 return $self->bail_on_events($self->editor->events)
3356 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3359 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3360 if $circ->renewal_remaining < 1;
3362 # -----------------------------------------------------------------
3364 $self->parent_circ($circ->id);
3365 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3368 # Opac renewal - re-use circ library from original circ (unless told not to)
3369 if($self->opac_renewal) {
3370 unless(defined($opac_renewal_use_circ_lib)) {
3371 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3372 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3373 $opac_renewal_use_circ_lib = 1;
3376 $opac_renewal_use_circ_lib = 0;
3379 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3382 # Run the fine generator against the old circ
3383 $self->generate_fines_start;
3385 $self->run_renew_permit;
3388 $self->do_checkin();
3389 return if $self->bail_out;
3391 unless( $self->permit_override ) {
3393 return if $self->bail_out;
3394 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3395 $self->remove_event('ITEM_NOT_CATALOGED');
3398 $self->override_events;
3399 return if $self->bail_out;
3402 $self->do_checkout();
3407 my( $self, $evt ) = @_;
3408 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3409 $logger->debug("circulator: removing event from list: $evt");
3410 my @events = @{$self->events};
3411 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3416 my( $self, $evt ) = @_;
3417 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3418 return grep { $_->{textcode} eq $evt } @{$self->events};
3423 sub run_renew_permit {
3426 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3427 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3428 $self->editor, $self->copy, $self->editor->requestor, 1
3430 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3433 if(!$self->legacy_script_support) {
3434 my $results = $self->run_indb_circ_test;
3435 $self->push_events($self->matrix_test_result_events)
3436 unless $self->circ_test_success;
3439 my $runner = $self->script_runner;
3441 $runner->load($self->circ_permit_renew);
3442 my $result = $runner->run or
3443 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3444 if ($result->{"events"}) {
3446 map { new OpenILS::Event($_) } @{$result->{"events"}}
3449 "circulator: circ_permit_renew for user " .
3450 $self->patron->id . " returned " .
3451 scalar(@{$result->{"events"}}) . " event(s)"
3455 $self->mk_script_runner;
3458 $logger->debug("circulator: re-creating script runner to be safe");
3462 # XXX: The primary mechanism for storing circ history is now handled
3463 # by tracking real circulation objects instead of bibs in a bucket.
3464 # However, this code is disabled by default and could be useful
3465 # some day, so may as well leave it for now.
3466 sub append_reading_list {
3470 $self->is_checkout and
3476 # verify history is globally enabled and uses the bucket mechanism
3477 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3478 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3480 return undef unless $htype and $htype eq 'bucket';
3482 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3484 # verify the patron wants to retain the hisory
3485 my $setting = $e->search_actor_user_setting(
3486 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3488 unless($setting and $setting->value) {
3493 my $bkt = $e->search_container_copy_bucket(
3494 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3499 # find the next item position
3500 my $last_item = $e->search_container_copy_bucket_item(
3501 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3502 $pos = $last_item->pos + 1 if $last_item;
3505 # create the history bucket if necessary
3506 $bkt = Fieldmapper::container::copy_bucket->new;
3507 $bkt->owner($self->patron->id);
3509 $bkt->btype('circ_history');
3511 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3514 my $item = Fieldmapper::container::copy_bucket_item->new;
3516 $item->bucket($bkt->id);
3517 $item->target_copy($self->copy->id);
3520 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3527 sub make_trigger_events {
3529 return unless $self->circ;
3530 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3531 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3532 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3537 sub checkin_handle_lost_now_found {
3538 my ($self, $bill_type) = @_;
3540 # ------------------------------------------------------------------
3541 # remove charge from patron's account if lost item is returned
3542 # ------------------------------------------------------------------
3544 my $bills = $self->editor->search_money_billing(
3546 xact => $self->circ->id,
3551 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3552 for my $bill (@$bills) {
3553 if( !$U->is_true($bill->voided) ) {
3554 $logger->info("lost item returned - voiding bill ".$bill->id);
3556 $bill->void_time('now');
3557 $bill->voider($self->editor->requestor->id);
3558 my $note = ($bill->note) ? $bill->note . "\n" : '';
3559 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3561 $self->bail_on_events($self->editor->event)
3562 unless $self->editor->update_money_billing($bill);
3567 sub checkin_handle_lost_now_found_restore_od {
3569 my $circ_lib = shift;
3571 # ------------------------------------------------------------------
3572 # restore those overdue charges voided when item was set to lost
3573 # ------------------------------------------------------------------
3575 my $ods = $self->editor->search_money_billing(
3577 xact => $self->circ->id,
3582 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3583 for my $bill (@$ods) {
3584 if( $U->is_true($bill->voided) ) {
3585 $logger->info("lost item returned - restoring overdue ".$bill->id);
3587 $bill->clear_void_time;
3588 $bill->voider($self->editor->requestor->id);
3589 my $note = ($bill->note) ? $bill->note . "\n" : '';
3590 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3592 $self->bail_on_events($self->editor->event)
3593 unless $self->editor->update_money_billing($bill);
3598 # ------------------------------------------------------------------
3599 # Lost-then-found item checked in. This sub generates new overdue
3600 # fines, beyond the point of any existing and possibly voided
3601 # overdue fines, up to the point of final checkin time (or max fine
3603 # ------------------------------------------------------------------
3604 sub generate_lost_overdue_fines {
3606 my $circ = $self->circ;
3607 my $e = $self->editor;
3609 # Re-open the transaction so the fine generator can see it
3610 if($circ->xact_finish or $circ->stop_fines) {
3612 $circ->clear_xact_finish;
3613 $circ->clear_stop_fines;
3614 $circ->clear_stop_fines_time;
3615 $e->update_action_circulation($circ) or return $e->die_event;
3619 $e->xact_begin; # generate_fines expects an in-xact editor
3620 $self->generate_fines;
3621 $circ = $self->circ; # generate fines re-fetches the circ
3625 # Re-close the transaction if no money is owed
3626 my ($obt) = $U->fetch_mbts($circ->id, $e);
3627 if ($obt and $obt->balance_owed == 0) {
3628 $circ->xact_finish('now');
3632 # Set stop fines if the fine generator didn't have to
3633 unless($circ->stop_fines) {
3634 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3635 $circ->stop_fines_time('now');
3639 # update the event data sent to the caller within the transaction
3640 $self->checkin_flesh_events;
3643 $e->update_action_circulation($circ) or return $e->die_event;