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;
18 sub determine_booking_status {
19 unless (defined $booking_status) {
20 my $ses = create OpenSRF::AppSession("router");
21 $booking_status = grep {$_ eq "open-ils.booking"} @{
22 $ses->request("opensrf.router.info.class.list")->gather(1)
25 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
28 return $booking_status;
34 flesh_fields => {acp => ['call_number'], acn => ['record']}
40 my $conf = OpenSRF::Utils::SettingsClient->new;
41 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
43 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
44 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
46 my $lb = $conf->config_value( @pfx2, 'script_path' );
47 $lb = [ $lb ] unless ref($lb);
50 return unless $legacy_script_support;
52 my @pfx = ( @pfx2, "scripts" );
53 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
54 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
55 my $d = $conf->config_value( @pfx, 'circ_duration' );
56 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
57 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
58 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
60 $logger->error( "Missing circ script(s)" )
61 unless( $p and $c and $d and $f and $m and $pr );
63 $scripts{circ_permit_patron} = $p;
64 $scripts{circ_permit_copy} = $c;
65 $scripts{circ_duration} = $d;
66 $scripts{circ_recurring_fines} = $f;
67 $scripts{circ_max_fines} = $m;
68 $scripts{circ_permit_renew} = $pr;
71 "circulator: Loaded rules scripts for circ: " .
72 "circ permit patron = $p, ".
73 "circ permit copy = $c, ".
74 "circ duration = $d, ".
75 "circ recurring fines = $f, " .
76 "circ max fines = $m, ".
77 "circ renew permit = $pr. ".
79 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
83 __PACKAGE__->register_method(
84 method => "run_method",
85 api_name => "open-ils.circ.checkout.permit",
87 Determines if the given checkout can occur
88 @param authtoken The login session key
89 @param params A trailing hash of named params including
90 barcode : The copy barcode,
91 patron : The patron the checkout is occurring for,
92 renew : true or false - whether or not this is a renewal
93 @return The event that occurred during the permit check.
97 __PACKAGE__->register_method (
98 method => 'run_method',
99 api_name => 'open-ils.circ.checkout.permit.override',
100 signature => q/@see open-ils.circ.checkout.permit/,
104 __PACKAGE__->register_method(
105 method => "run_method",
106 api_name => "open-ils.circ.checkout",
109 @param authtoken The login session key
110 @param params A named hash of params including:
112 barcode If no copy is provided, the copy is retrieved via barcode
113 copyid If no copy or barcode is provide, the copy id will be use
114 patron The patron's id
115 noncat True if this is a circulation for a non-cataloted item
116 noncat_type The non-cataloged type id
117 noncat_circ_lib The location for the noncat circ.
118 precat The item has yet to be cataloged
119 dummy_title The temporary title of the pre-cataloded item
120 dummy_author The temporary authr of the pre-cataloded item
121 Default is the home org of the staff member
122 @return The SUCCESS event on success, any other event depending on the error
125 __PACKAGE__->register_method(
126 method => "run_method",
127 api_name => "open-ils.circ.checkin",
130 Generic super-method for handling all copies
131 @param authtoken The login session key
132 @param params Hash of named parameters including:
133 barcode - The copy barcode
134 force - If true, copies in bad statuses will be checked in and give good statuses
135 noop - don't capture holds or put items into transit
136 void_overdues - void all overdues for the circulation (aka amnesty)
141 __PACKAGE__->register_method(
142 method => "run_method",
143 api_name => "open-ils.circ.checkin.override",
144 signature => q/@see open-ils.circ.checkin/
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.renew.override",
150 signature => q/@see open-ils.circ.renew/,
154 __PACKAGE__->register_method(
155 method => "run_method",
156 api_name => "open-ils.circ.renew",
157 notes => <<" NOTES");
158 PARAMS( authtoken, circ => circ_id );
159 open-ils.circ.renew(login_session, circ_object);
160 Renews the provided circulation. login_session is the requestor of the
161 renewal and if the logged in user is not the same as circ->usr, then
162 the logged in user must have RENEW_CIRC permissions.
165 __PACKAGE__->register_method(
166 method => "run_method",
167 api_name => "open-ils.circ.checkout.full"
169 __PACKAGE__->register_method(
170 method => "run_method",
171 api_name => "open-ils.circ.checkout.full.override"
173 __PACKAGE__->register_method(
174 method => "run_method",
175 api_name => "open-ils.circ.reservation.pickup"
177 __PACKAGE__->register_method(
178 method => "run_method",
179 api_name => "open-ils.circ.reservation.return"
181 __PACKAGE__->register_method(
182 method => "run_method",
183 api_name => "open-ils.circ.reservation.return.override"
185 __PACKAGE__->register_method(
186 method => "run_method",
187 api_name => "open-ils.circ.checkout.inspect",
188 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
193 my( $self, $conn, $auth, $args ) = @_;
194 translate_legacy_args($args);
195 my $api = $self->api_name;
198 OpenILS::Application::Circ::Circulator->new($auth, %$args);
200 return circ_events($circulator) if $circulator->bail_out;
202 $circulator->use_booking(determine_booking_status());
204 # --------------------------------------------------------------------------
205 # First, check for a booking transit, as the barcode may not be a copy
206 # barcode, but a resource barcode, and nothing else in here will work
207 # --------------------------------------------------------------------------
209 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
210 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
211 if (@$resources) { # yes!
213 my $res_id_list = [ map { $_->id } @$resources ];
214 my $transit = $circulator->editor->search_action_reservation_transit_copy(
216 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
217 { order_by => { artc => 'source_send_time' }, limit => 1 }
219 )->[0]; # Any transit for this barcode?
221 if ($transit) { # yes! unwrap it.
223 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
224 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
226 my $success_event = new OpenILS::Event(
227 "SUCCESS", "payload" => {"reservation" => $reservation}
229 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
230 if (my $copy = $circulator->editor->search_asset_copy([
231 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
232 ])->[0]) { # got a copy
233 $copy->status( $transit->copy_status );
234 $copy->editor($circulator->editor->requestor->id);
235 $copy->edit_date('now');
236 $circulator->editor->update_asset_copy($copy);
237 $success_event->{"payload"}->{"record"} =
238 $U->record_to_mvr($copy->call_number->record);
239 $copy->call_number($copy->call_number->id);
240 $success_event->{"payload"}->{"copy"} = $copy;
244 $transit->dest_recv_time('now');
245 $circulator->editor->update_action_reservation_transit_copy( $transit );
247 $circulator->editor->commit;
248 # Formerly this branch just stopped here. Argh!
249 $conn->respond_complete($success_event);
257 # --------------------------------------------------------------------------
258 # Go ahead and load the script runner to make sure we have all
259 # of the objects we need
260 # --------------------------------------------------------------------------
262 if ($circulator->use_booking) {
263 $circulator->is_res_checkin($circulator->is_checkin(1))
264 if $api =~ /reservation.return/ or (
265 $api =~ /checkin/ and $circulator->seems_like_reservation()
268 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
271 $circulator->is_renewal(1) if $api =~ /renew/;
272 $circulator->is_checkin(1) if $api =~ /checkin/;
274 $circulator->mk_env();
275 $circulator->noop(1) if $circulator->claims_never_checked_out;
277 if($legacy_script_support and not $circulator->is_checkin) {
278 $circulator->mk_script_runner();
279 $circulator->legacy_script_support(1);
280 $circulator->circ_permit_patron($scripts{circ_permit_patron});
281 $circulator->circ_permit_copy($scripts{circ_permit_copy});
282 $circulator->circ_duration($scripts{circ_duration});
283 $circulator->circ_permit_renew($scripts{circ_permit_renew});
285 return circ_events($circulator) if $circulator->bail_out;
288 $circulator->override(1) if $api =~ /override/o;
290 if( $api =~ /checkout\.permit/ ) {
291 $circulator->do_permit();
293 } elsif( $api =~ /checkout.full/ ) {
295 # requesting a precat checkout implies that any required
296 # overrides have been performed. Go ahead and re-override.
297 $circulator->skip_permit_key(1);
298 $circulator->override(1) if $circulator->request_precat;
299 $circulator->do_permit();
300 $circulator->is_checkout(1);
301 unless( $circulator->bail_out ) {
302 $circulator->events([]);
303 $circulator->do_checkout();
306 } elsif( $circulator->is_res_checkout ) {
307 $circulator->do_reservation_pickup();
309 } elsif( $api =~ /inspect/ ) {
310 my $data = $circulator->do_inspect();
311 $circulator->editor->rollback;
314 } elsif( $api =~ /checkout/ ) {
315 $circulator->is_checkout(1);
316 $circulator->do_checkout();
318 } elsif( $circulator->is_res_checkin ) {
319 $circulator->do_reservation_return();
320 $circulator->do_checkin() if ($circulator->copy());
321 } elsif( $api =~ /checkin/ ) {
322 $circulator->do_checkin();
324 } elsif( $api =~ /renew/ ) {
325 $circulator->is_renewal(1);
326 $circulator->do_renew();
329 if( $circulator->bail_out ) {
332 # make sure no success event accidentally slip in
334 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
337 my @e = @{$circulator->events};
338 push( @ee, $_->{textcode} ) for @e;
339 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
341 $circulator->editor->rollback;
344 $circulator->editor->commit;
347 $circulator->script_runner->cleanup if $circulator->script_runner;
349 $conn->respond_complete(circ_events($circulator));
351 unless($circulator->bail_out) {
352 $circulator->do_hold_notify($circulator->notify_hold)
353 if $circulator->notify_hold;
354 $circulator->retarget_holds if $circulator->retarget;
355 $circulator->append_reading_list;
356 $circulator->make_trigger_events;
362 my @e = @{$circ->events};
363 # if we have multiple events, SUCCESS should not be one of them;
364 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
365 return (@e == 1) ? $e[0] : \@e;
369 sub translate_legacy_args {
372 if( $$args{barcode} ) {
373 $$args{copy_barcode} = $$args{barcode};
374 delete $$args{barcode};
377 if( $$args{copyid} ) {
378 $$args{copy_id} = $$args{copyid};
379 delete $$args{copyid};
382 if( $$args{patronid} ) {
383 $$args{patron_id} = $$args{patronid};
384 delete $$args{patronid};
387 if( $$args{patron} and !ref($$args{patron}) ) {
388 $$args{patron_id} = $$args{patron};
389 delete $$args{patron};
393 if( $$args{noncat} ) {
394 $$args{is_noncat} = $$args{noncat};
395 delete $$args{noncat};
398 if( $$args{precat} ) {
399 $$args{is_precat} = $$args{request_precat} = $$args{precat};
400 delete $$args{precat};
406 # --------------------------------------------------------------------------
407 # This package actually manages all of the circulation logic
408 # --------------------------------------------------------------------------
409 package OpenILS::Application::Circ::Circulator;
410 use strict; use warnings;
411 use vars q/$AUTOLOAD/;
413 use OpenILS::Utils::Fieldmapper;
414 use OpenSRF::Utils::Cache;
415 use Digest::MD5 qw(md5_hex);
416 use DateTime::Format::ISO8601;
417 use OpenILS::Utils::PermitHold;
418 use OpenSRF::Utils qw/:datetime/;
419 use OpenSRF::Utils::SettingsClient;
420 use OpenILS::Application::Circ::Holds;
421 use OpenILS::Application::Circ::Transit;
422 use OpenSRF::Utils::Logger qw(:logger);
423 use OpenILS::Utils::CStoreEditor qw/:funcs/;
424 use OpenILS::Application::Circ::ScriptBuilder;
425 use OpenILS::Const qw/:const/;
426 use OpenILS::Utils::Penalty;
427 use OpenILS::Application::Circ::CircCommon;
430 my $holdcode = "OpenILS::Application::Circ::Holds";
431 my $transcode = "OpenILS::Application::Circ::Transit";
437 # --------------------------------------------------------------------------
438 # Add a pile of automagic getter/setter methods
439 # --------------------------------------------------------------------------
440 my @AUTOLOAD_FIELDS = qw/
487 recurring_fines_level
500 cancelled_hold_transit
507 circ_matrix_matchpoint
509 legacy_script_support
519 claims_never_checked_out
529 my $type = ref($self) or die "$self is not an object";
531 my $name = $AUTOLOAD;
534 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
535 $logger->error("circulator: $type: invalid autoload field: $name");
536 die "$type: invalid autoload field: $name\n"
541 *{"${type}::${name}"} = sub {
544 $s->{$name} = $v if defined $v;
548 return $self->$name($data);
553 my( $class, $auth, %args ) = @_;
554 $class = ref($class) || $class;
555 my $self = bless( {}, $class );
558 $self->editor(new_editor(xact => 1, authtoken => $auth));
560 unless( $self->editor->checkauth ) {
561 $self->bail_on_events($self->editor->event);
565 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
567 $self->$_($args{$_}) for keys %args;
570 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
572 # if this is a renewal, default to desk_renewal
573 $self->desk_renewal(1) unless
574 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
576 $self->capture('') unless $self->capture;
578 unless(%user_groups) {
579 my $gps = $self->editor->retrieve_all_permission_grp_tree;
580 %user_groups = map { $_->id => $_ } @$gps;
587 # --------------------------------------------------------------------------
588 # True if we should discontinue processing
589 # --------------------------------------------------------------------------
591 my( $self, $bool ) = @_;
592 if( defined $bool ) {
593 $logger->info("circulator: BAILING OUT") if $bool;
594 $self->{bail_out} = $bool;
596 return $self->{bail_out};
601 my( $self, @evts ) = @_;
604 $e->{payload} = $self->copy if
605 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
607 $logger->info("circulator: pushing event ".$e->{textcode});
608 push( @{$self->events}, $e ) unless
609 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
615 return '' if $self->skip_permit_key;
616 my $key = md5_hex( time() . rand() . "$$" );
617 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
618 return $self->permit_key($key);
621 sub check_permit_key {
623 return 1 if $self->skip_permit_key;
624 my $key = $self->permit_key;
625 return 0 unless $key;
626 my $k = "oils_permit_key_$key";
627 my $one = $self->cache_handle->get_cache($k);
628 $self->cache_handle->delete_cache($k);
629 return ($one) ? 1 : 0;
632 sub seems_like_reservation {
635 # Some words about the following method:
636 # 1) It requires the VIEW_USER permission, but that's not an
637 # issue, right, since all staff should have that?
638 # 2) It returns only one reservation at a time, even if an item can be
639 # and is currently overbooked. Hmmm....
640 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
641 my $result = $booking_ses->request(
642 "open-ils.booking.reservations.by_returnable_resource_barcode",
643 $self->editor->authtoken,
646 $booking_ses->disconnect;
648 return $self->bail_on_events($result) if defined $U->event_code($result);
651 $self->reservation(shift @$result);
659 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
660 sub save_trimmed_copy {
661 my ($self, $copy) = @_;
664 $self->volume($copy->call_number);
665 $self->title($self->volume->record);
666 $self->copy->call_number($self->volume->id);
667 $self->volume->record($self->title->id);
668 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
669 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
670 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
671 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
677 my $e = $self->editor;
679 # --------------------------------------------------------------------------
680 # Grab the fleshed copy
681 # --------------------------------------------------------------------------
682 unless($self->is_noncat) {
685 $copy = $e->retrieve_asset_copy(
686 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
688 } elsif( $self->copy_barcode ) {
690 $copy = $e->search_asset_copy(
691 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
692 } elsif( $self->reservation ) {
693 my $res = $e->json_query(
695 "select" => {"acp" => ["id"]},
700 "field" => "barcode",
704 "field" => "current_resource"
712 "id" => (ref $self->reservation) ?
713 $self->reservation->id : $self->reservation
718 if (ref $res eq "ARRAY" and scalar @$res) {
719 $logger->info("circulator: mapped reservation " .
720 $self->reservation . " to copy " . $res->[0]->{"id"});
721 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
726 $self->save_trimmed_copy($copy);
728 # We can't renew if there is no copy
729 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
730 if $self->is_renewal;
735 # --------------------------------------------------------------------------
737 # --------------------------------------------------------------------------
741 flesh_fields => {au => [ qw/ card / ]}
744 if( $self->patron_id ) {
745 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
746 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
748 } elsif( $self->patron_barcode ) {
750 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
751 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
752 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
754 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
755 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
758 if( my $copy = $self->copy ) {
761 $flesh->{flesh_fields}->{circ} = ['usr'];
763 my $circ = $e->search_action_circulation([
764 {target_copy => $copy->id, checkin_time => undef}, $flesh
768 $patron = $circ->usr;
769 $circ->usr($patron->id); # de-flesh for consistency
775 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
776 unless $self->patron($patron) or $self->is_checkin;
778 unless($self->is_checkin) {
780 # Check for inactivity and patron reg. expiration
782 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
783 unless $U->is_true($patron->active);
785 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
786 unless $U->is_true($patron->card->active);
788 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
789 cleanse_ISO8601($patron->expire_date));
791 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
792 if( CORE::time > $expire->epoch ) ;
796 # --------------------------------------------------------------------------
797 # This builds the script runner environment and fetches most of the
799 # --------------------------------------------------------------------------
800 sub mk_script_runner {
806 qw/copy copy_barcode copy_id patron
807 patron_id patron_barcode volume title editor/;
809 # Translate our objects into the ScriptBuilder args hash
810 $$args{$_} = $self->$_() for @fields;
812 $args->{ignore_user_status} = 1 if $self->is_checkin;
813 $$args{fetch_patron_by_circ_copy} = 1;
814 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
816 if( my $pco = $self->pending_checkouts ) {
817 $logger->info("circulator: we were given a pending checkouts number of $pco");
818 $$args{patronItemsOut} = $pco;
821 # This fetches most of the objects we need
822 $self->script_runner(
823 OpenILS::Application::Circ::ScriptBuilder->build($args));
825 # Now we translate the ScriptBuilder objects back into self
826 $self->$_($$args{$_}) for @fields;
828 my @evts = @{$args->{_events}} if $args->{_events};
830 $logger->debug("circulator: script builder returned events: @evts") if @evts;
834 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
835 if(!$self->is_noncat and
837 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
841 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
842 return $self->bail_on_events(@e);
847 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
848 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
849 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
850 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
854 # We can't renew if there is no copy
855 return $self->bail_on_events(@evts) if
856 $self->is_renewal and !$self->copy;
858 # Set some circ-specific flags in the script environment
859 my $evt = "environment";
860 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
862 if( $self->is_noncat ) {
863 $self->script_runner->insert("$evt.isNonCat", 1);
864 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
867 if( $self->is_precat ) {
868 $self->script_runner->insert("environment.isPrecat", 1, 1);
871 $self->script_runner->add_path( $_ ) for @$script_libs;
876 # --------------------------------------------------------------------------
877 # Does the circ permit work
878 # --------------------------------------------------------------------------
882 $self->log_me("do_permit()");
884 unless( $self->editor->requestor->id == $self->patron->id ) {
885 return $self->bail_on_events($self->editor->event)
886 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
889 $self->check_captured_holds();
890 $self->do_copy_checks();
891 return if $self->bail_out;
892 $self->run_patron_permit_scripts();
893 $self->run_copy_permit_scripts()
894 unless $self->is_precat or $self->is_noncat;
895 $self->check_item_deposit_events();
896 $self->override_events();
897 return if $self->bail_out;
899 if($self->is_precat and not $self->request_precat) {
902 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
903 return $self->bail_out(1) unless $self->is_renewal;
907 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
910 sub check_item_deposit_events {
912 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
913 if $self->is_deposit and not $self->is_deposit_exempt;
914 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
915 if $self->is_rental and not $self->is_rental_exempt;
918 # returns true if the user is not required to pay deposits
919 sub is_deposit_exempt {
921 my $pid = (ref $self->patron->profile) ?
922 $self->patron->profile->id : $self->patron->profile;
923 my $groups = $U->ou_ancestor_setting_value(
924 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
925 for my $grp (@$groups) {
926 return 1 if $self->is_group_descendant($grp, $pid);
931 # returns true if the user is not required to pay rental fees
932 sub is_rental_exempt {
934 my $pid = (ref $self->patron->profile) ?
935 $self->patron->profile->id : $self->patron->profile;
936 my $groups = $U->ou_ancestor_setting_value(
937 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
938 for my $grp (@$groups) {
939 return 1 if $self->is_group_descendant($grp, $pid);
944 sub is_group_descendant {
945 my($self, $p_id, $c_id) = @_;
946 return 0 unless defined $p_id and defined $c_id;
947 return 1 if $c_id == $p_id;
948 while(my $grp = $user_groups{$c_id}) {
949 $c_id = $grp->parent;
950 return 0 unless defined $c_id;
951 return 1 if $c_id == $p_id;
956 sub check_captured_holds {
958 my $copy = $self->copy;
959 my $patron = $self->patron;
961 return undef unless $copy;
963 my $s = $U->copy_status($copy->status)->id;
964 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
965 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
967 # Item is on the holds shelf, make sure it's going to the right person
968 my $holds = $self->editor->search_action_hold_request(
971 current_copy => $copy->id ,
972 capture_time => { '!=' => undef },
973 cancel_time => undef,
974 fulfillment_time => undef
980 if( $holds and $$holds[0] ) {
981 return undef if $$holds[0]->usr == $patron->id;
984 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
986 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
992 my $copy = $self->copy;
995 my $stat = $U->copy_status($copy->status)->id;
997 # We cannot check out a copy if it is in-transit
998 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
999 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1002 $self->handle_claims_returned();
1003 return if $self->bail_out;
1005 # no claims returned circ was found, check if there is any open circ
1006 unless( $self->is_renewal ) {
1008 my $circs = $self->editor->search_action_circulation(
1009 { target_copy => $copy->id, checkin_time => undef }
1012 if(my $old_circ = $circs->[0]) { # an open circ was found
1014 my $payload = {copy => $copy};
1016 if($old_circ->usr == $self->patron->id) {
1018 $payload->{old_circ} = $old_circ;
1020 # If there is an open circulation on the checkout item and an auto-renew
1021 # interval is defined, inform the caller that they should go
1022 # ahead and renew the item instead of warning about open circulations.
1024 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1026 'circ.checkout_auto_renew_age',
1030 if($auto_renew_intvl) {
1031 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1032 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1034 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1035 $payload->{auto_renew} = 1;
1040 return $self->bail_on_events(
1041 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1047 my $LEGACY_CIRC_EVENT_MAP = {
1048 'no_item' => 'ITEM_NOT_CATALOGED',
1049 'actor.usr.barred' => 'PATRON_BARRED',
1050 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1051 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1052 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1053 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1054 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1055 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1056 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1057 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1061 # ---------------------------------------------------------------------
1062 # This pushes any patron-related events into the list but does not
1063 # set bail_out for any events
1064 # ---------------------------------------------------------------------
1065 sub run_patron_permit_scripts {
1067 my $runner = $self->script_runner;
1068 my $patronid = $self->patron->id;
1072 if(!$self->legacy_script_support) {
1074 my $results = $self->run_indb_circ_test;
1075 unless($self->circ_test_success) {
1076 # no_item result is OK during noncat checkout
1077 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1078 push @allevents, $self->matrix_test_result_events;
1084 # ---------------------------------------------------------------------
1085 # # Now run the patron permit script
1086 # ---------------------------------------------------------------------
1087 $runner->load($self->circ_permit_patron);
1088 my $result = $runner->run or
1089 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1091 my $patron_events = $result->{events};
1093 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1094 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1095 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1096 $penalties = $penalties->{fatal_penalties};
1098 for my $pen (@$penalties) {
1099 my $event = OpenILS::Event->new($pen->name);
1100 $event->{desc} = $pen->label;
1101 push(@allevents, $event);
1104 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1108 $_->{payload} = $self->copy if
1109 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1112 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1114 $self->push_events(@allevents);
1117 sub matrix_test_result_codes {
1119 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1122 sub matrix_test_result_events {
1125 my $event = new OpenILS::Event(
1126 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1128 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1130 } (@{$self->matrix_test_result});
1133 sub run_indb_circ_test {
1135 return $self->matrix_test_result if $self->matrix_test_result;
1137 my $dbfunc = ($self->is_renewal) ?
1138 'action.item_user_renew_test' : 'action.item_user_circ_test';
1140 if( $self->is_precat && $self->request_precat) {
1141 $self->make_precat_copy;
1142 return if $self->bail_out;
1145 my $results = $self->editor->json_query(
1149 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1155 $self->circ_test_success($U->is_true($results->[0]->{success}));
1157 if(my $mp = $results->[0]->{matchpoint}) {
1158 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1159 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1160 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1161 if($results->[0]->{renewals}) {
1162 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1164 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1165 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1166 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1169 return $self->matrix_test_result($results);
1172 # ---------------------------------------------------------------------
1173 # given a use and copy, this will calculate the circulation policy
1174 # parameters. Only works with in-db circ.
1175 # ---------------------------------------------------------------------
1179 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1181 $self->run_indb_circ_test;
1184 circ_test_success => $self->circ_test_success,
1185 failure_events => [],
1186 failure_codes => [],
1187 matchpoint => $self->circ_matrix_matchpoint
1190 unless($self->circ_test_success) {
1191 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1192 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1195 if($self->circ_matrix_matchpoint) {
1196 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1197 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1198 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1199 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1201 my $policy = $self->get_circ_policy(
1202 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1204 $$results{$_} = $$policy{$_} for keys %$policy;
1210 # ---------------------------------------------------------------------
1211 # Loads the circ policy info for duration, recurring fine, and max
1212 # fine based on the current copy
1213 # ---------------------------------------------------------------------
1214 sub get_circ_policy {
1215 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1218 duration_rule => $duration_rule->name,
1219 recurring_fine_rule => $recurring_fine_rule->name,
1220 max_fine_rule => $max_fine_rule->name,
1221 max_fine => $self->get_max_fine_amount($max_fine_rule),
1222 fine_interval => $recurring_fine_rule->recurrence_interval,
1223 renewal_remaining => $duration_rule->max_renewals
1226 if($hard_due_date) {
1227 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1228 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1231 $policy->{duration_date_ceiling} = undef;
1232 $policy->{duration_date_ceiling_force} = undef;
1235 $policy->{duration} = $duration_rule->shrt
1236 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1237 $policy->{duration} = $duration_rule->normal
1238 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1239 $policy->{duration} = $duration_rule->extended
1240 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1242 $policy->{recurring_fine} = $recurring_fine_rule->low
1243 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1244 $policy->{recurring_fine} = $recurring_fine_rule->normal
1245 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1246 $policy->{recurring_fine} = $recurring_fine_rule->high
1247 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1252 sub get_max_fine_amount {
1254 my $max_fine_rule = shift;
1255 my $max_amount = $max_fine_rule->amount;
1257 # if is_percent is true then the max->amount is
1258 # use as a percentage of the copy price
1259 if ($U->is_true($max_fine_rule->is_percent)) {
1260 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1261 $max_amount = $price * $max_fine_rule->amount / 100;
1263 $U->ou_ancestor_setting_value(
1265 'circ.max_fine.cap_at_price',
1269 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1270 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1278 sub run_copy_permit_scripts {
1280 my $copy = $self->copy || return;
1281 my $runner = $self->script_runner;
1285 if(!$self->legacy_script_support) {
1286 my $results = $self->run_indb_circ_test;
1287 push @allevents, $self->matrix_test_result_events
1288 unless $self->circ_test_success;
1291 # ---------------------------------------------------------------------
1292 # Capture all of the copy permit events
1293 # ---------------------------------------------------------------------
1294 $runner->load($self->circ_permit_copy);
1295 my $result = $runner->run or
1296 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1297 my $copy_events = $result->{events};
1299 # ---------------------------------------------------------------------
1300 # Now collect all of the events together
1301 # ---------------------------------------------------------------------
1302 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1305 # See if this copy has an alert message
1306 my $ae = $self->check_copy_alert();
1307 push( @allevents, $ae ) if $ae;
1309 # uniquify the events
1310 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1311 @allevents = values %hash;
1313 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1315 $self->push_events(@allevents);
1319 sub check_copy_alert {
1321 return undef if $self->is_renewal;
1322 return OpenILS::Event->new(
1323 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1324 if $self->copy and $self->copy->alert_message;
1330 # --------------------------------------------------------------------------
1331 # If the call is overriding and has permissions to override every collected
1332 # event, the are cleared. Any event that the caller does not have
1333 # permission to override, will be left in the event list and bail_out will
1335 # XXX We need code in here to cancel any holds/transits on copies
1336 # that are being force-checked out
1337 # --------------------------------------------------------------------------
1338 sub override_events {
1340 my @events = @{$self->events};
1341 return unless @events;
1343 if(!$self->override) {
1344 return $self->bail_out(1)
1345 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1350 for my $e (@events) {
1351 my $tc = $e->{textcode};
1352 next if $tc eq 'SUCCESS';
1353 my $ov = "$tc.override";
1354 $logger->info("circulator: attempting to override event: $ov");
1356 return $self->bail_on_events($self->editor->event)
1357 unless( $self->editor->allowed($ov) );
1362 # --------------------------------------------------------------------------
1363 # If there is an open claimsreturn circ on the requested copy, close the
1364 # circ if overriding, otherwise bail out
1365 # --------------------------------------------------------------------------
1366 sub handle_claims_returned {
1368 my $copy = $self->copy;
1370 my $CR = $self->editor->search_action_circulation(
1372 target_copy => $copy->id,
1373 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1374 checkin_time => undef,
1378 return unless ($CR = $CR->[0]);
1382 # - If the caller has set the override flag, we will check the item in
1383 if($self->override) {
1385 $CR->checkin_time('now');
1386 $CR->checkin_scan_time('now');
1387 $CR->checkin_lib($self->circ_lib);
1388 $CR->checkin_workstation($self->editor->requestor->wsid);
1389 $CR->checkin_staff($self->editor->requestor->id);
1391 $evt = $self->editor->event
1392 unless $self->editor->update_action_circulation($CR);
1395 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1398 $self->bail_on_events($evt) if $evt;
1403 # --------------------------------------------------------------------------
1404 # This performs the checkout
1405 # --------------------------------------------------------------------------
1409 $self->log_me("do_checkout()");
1411 # make sure perms are good if this isn't a renewal
1412 unless( $self->is_renewal ) {
1413 return $self->bail_on_events($self->editor->event)
1414 unless( $self->editor->allowed('COPY_CHECKOUT') );
1417 # verify the permit key
1418 unless( $self->check_permit_key ) {
1419 if( $self->permit_override ) {
1420 return $self->bail_on_events($self->editor->event)
1421 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1423 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1427 # if this is a non-cataloged circ, build the circ and finish
1428 if( $self->is_noncat ) {
1429 $self->checkout_noncat;
1431 OpenILS::Event->new('SUCCESS',
1432 payload => { noncat_circ => $self->circ }));
1436 if( $self->is_precat ) {
1437 $self->make_precat_copy;
1438 return if $self->bail_out;
1440 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1441 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1444 $self->do_copy_checks;
1445 return if $self->bail_out;
1447 $self->run_checkout_scripts();
1448 return if $self->bail_out;
1450 $self->build_checkout_circ_object();
1451 return if $self->bail_out;
1453 my $modify_to_start = $self->booking_adjusted_due_date();
1454 return if $self->bail_out;
1456 $self->apply_modified_due_date($modify_to_start);
1457 return if $self->bail_out;
1459 return $self->bail_on_events($self->editor->event)
1460 unless $self->editor->create_action_circulation($self->circ);
1462 # refresh the circ to force local time zone for now
1463 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1465 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1467 return if $self->bail_out;
1469 $self->apply_deposit_fee();
1470 return if $self->bail_out;
1472 $self->handle_checkout_holds();
1473 return if $self->bail_out;
1475 # ------------------------------------------------------------------------------
1476 # Update the patron penalty info in the DB. Run it for permit-overrides
1477 # since the penalties are not updated during the permit phase
1478 # ------------------------------------------------------------------------------
1479 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1481 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1484 if($self->is_renewal) {
1485 # flesh the billing summary for the checked-in circ
1486 $pcirc = $self->editor->retrieve_action_circulation([
1488 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1493 OpenILS::Event->new('SUCCESS',
1495 copy => $U->unflesh_copy($self->copy),
1496 circ => $self->circ,
1498 holds_fulfilled => $self->fulfilled_holds,
1499 deposit_billing => $self->deposit_billing,
1500 rental_billing => $self->rental_billing,
1501 parent_circ => $pcirc,
1502 patron => ($self->return_patron) ? $self->patron : undef,
1503 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1509 sub apply_deposit_fee {
1511 my $copy = $self->copy;
1513 ($self->is_deposit and not $self->is_deposit_exempt) or
1514 ($self->is_rental and not $self->is_rental_exempt);
1516 return if $self->is_deposit and $self->skip_deposit_fee;
1517 return if $self->is_rental and $self->skip_rental_fee;
1519 my $bill = Fieldmapper::money::billing->new;
1520 my $amount = $copy->deposit_amount;
1524 if($self->is_deposit) {
1525 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1527 $self->deposit_billing($bill);
1529 $billing_type = OILS_BILLING_TYPE_RENTAL;
1531 $self->rental_billing($bill);
1534 $bill->xact($self->circ->id);
1535 $bill->amount($amount);
1536 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1537 $bill->billing_type($billing_type);
1538 $bill->btype($btype);
1539 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1541 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1546 my $copy = $self->copy;
1548 my $stat = $copy->status if ref $copy->status;
1549 my $loc = $copy->location if ref $copy->location;
1550 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1552 $copy->status($stat->id) if $stat;
1553 $copy->location($loc->id) if $loc;
1554 $copy->circ_lib($circ_lib->id) if $circ_lib;
1555 $copy->editor($self->editor->requestor->id);
1556 $copy->edit_date('now');
1557 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1559 return $self->bail_on_events($self->editor->event)
1560 unless $self->editor->update_asset_copy($self->copy);
1562 $copy->status($U->copy_status($copy->status));
1563 $copy->location($loc) if $loc;
1564 $copy->circ_lib($circ_lib) if $circ_lib;
1567 sub update_reservation {
1569 my $reservation = $self->reservation;
1571 my $usr = $reservation->usr;
1572 my $target_rt = $reservation->target_resource_type;
1573 my $target_r = $reservation->target_resource;
1574 my $current_r = $reservation->current_resource;
1576 $reservation->usr($usr->id) if ref $usr;
1577 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1578 $reservation->target_resource($target_r->id) if ref $target_r;
1579 $reservation->current_resource($current_r->id) if ref $current_r;
1581 return $self->bail_on_events($self->editor->event)
1582 unless $self->editor->update_booking_reservation($self->reservation);
1585 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1586 $self->reservation($reservation);
1590 sub bail_on_events {
1591 my( $self, @evts ) = @_;
1592 $self->push_events(@evts);
1597 # ------------------------------------------------------------------------------
1598 # When an item is checked out, see if we can fulfill a hold for this patron
1599 # ------------------------------------------------------------------------------
1600 sub handle_checkout_holds {
1602 my $copy = $self->copy;
1603 my $patron = $self->patron;
1605 my $e = $self->editor;
1606 $self->fulfilled_holds([]);
1608 # pre/non-cats can't fulfill a hold
1609 return if $self->is_precat or $self->is_noncat;
1611 my $hold = $e->search_action_hold_request({
1612 current_copy => $copy->id ,
1613 cancel_time => undef,
1614 fulfillment_time => undef,
1616 {expire_time => undef},
1617 {expire_time => {'>' => 'now'}}
1621 if($hold and $hold->usr != $patron->id) {
1622 # reset the hold since the copy is now checked out
1624 $logger->info("circulator: un-targeting hold ".$hold->id.
1625 " because copy ".$copy->id." is getting checked out");
1627 $hold->clear_prev_check_time;
1628 $hold->clear_current_copy;
1629 $hold->clear_capture_time;
1631 return $self->bail_on_event($e->event)
1632 unless $e->update_action_hold_request($hold);
1638 $hold = $self->find_related_user_hold($copy, $patron) or return;
1639 $logger->info("circulator: found related hold to fulfill in checkout");
1642 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1644 # if the hold was never officially captured, capture it.
1645 $hold->current_copy($copy->id);
1646 $hold->capture_time('now') unless $hold->capture_time;
1647 $hold->fulfillment_time('now');
1648 $hold->fulfillment_staff($e->requestor->id);
1649 $hold->fulfillment_lib($self->circ_lib);
1651 return $self->bail_on_events($e->event)
1652 unless $e->update_action_hold_request($hold);
1654 $holdcode->delete_hold_copy_maps($e, $hold->id);
1655 return $self->fulfilled_holds([$hold->id]);
1659 # ------------------------------------------------------------------------------
1660 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1661 # the patron directly targets the checked out item, see if there is another hold
1662 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1663 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1664 # ------------------------------------------------------------------------------
1665 sub find_related_user_hold {
1666 my($self, $copy, $patron) = @_;
1667 my $e = $self->editor;
1669 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1671 return undef unless $U->ou_ancestor_setting_value(
1672 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1674 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1676 select => {ahr => ['id']},
1681 fkey => 'current_copy',
1682 type => 'left' # there may be no current_copy
1689 fulfillment_time => undef,
1690 cancel_time => undef,
1692 {expire_time => undef},
1693 {expire_time => {'>' => 'now'}}
1700 target => $self->volume->id
1706 target => $self->title->id
1712 {id => undef}, # left-join copy may be nonexistent
1713 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1717 order_by => {ahr => {request_time => {direction => 'asc'}}},
1721 my $hold_info = $e->json_query($args)->[0];
1722 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1727 sub run_checkout_scripts {
1732 my $runner = $self->script_runner;
1741 my $hard_due_date_name;
1743 if(!$self->legacy_script_support) {
1744 $self->run_indb_circ_test();
1745 $duration = $self->circ_matrix_matchpoint->duration_rule;
1746 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1747 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1748 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1752 $runner->load($self->circ_duration);
1754 my $result = $runner->run or
1755 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1757 $duration_name = $result->{durationRule};
1758 $recurring_name = $result->{recurringFinesRule};
1759 $max_fine_name = $result->{maxFine};
1760 $hard_due_date_name = $result->{hardDueDate};
1763 $duration_name = $duration->name if $duration;
1764 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1767 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1768 return $self->bail_on_events($evt) if ($evt && !$nobail);
1770 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1771 return $self->bail_on_events($evt) if ($evt && !$nobail);
1773 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1774 return $self->bail_on_events($evt) if ($evt && !$nobail);
1776 if($hard_due_date_name) {
1777 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1778 return $self->bail_on_events($evt) if ($evt && !$nobail);
1784 # The item circulates with an unlimited duration
1788 $hard_due_date = undef;
1791 $self->duration_rule($duration);
1792 $self->recurring_fines_rule($recurring);
1793 $self->max_fine_rule($max_fine);
1794 $self->hard_due_date($hard_due_date);
1798 sub build_checkout_circ_object {
1801 my $circ = Fieldmapper::action::circulation->new;
1802 my $duration = $self->duration_rule;
1803 my $max = $self->max_fine_rule;
1804 my $recurring = $self->recurring_fines_rule;
1805 my $hard_due_date = $self->hard_due_date;
1806 my $copy = $self->copy;
1807 my $patron = $self->patron;
1808 my $duration_date_ceiling;
1809 my $duration_date_ceiling_force;
1813 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1814 $duration_date_ceiling = $policy->{duration_date_ceiling};
1815 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1817 my $dname = $duration->name;
1818 my $mname = $max->name;
1819 my $rname = $recurring->name;
1821 if($hard_due_date) {
1822 $hdname = $hard_due_date->name;
1825 $logger->debug("circulator: building circulation ".
1826 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1828 $circ->duration($policy->{duration});
1829 $circ->recurring_fine($policy->{recurring_fine});
1830 $circ->duration_rule($duration->name);
1831 $circ->recurring_fine_rule($recurring->name);
1832 $circ->max_fine_rule($max->name);
1833 $circ->max_fine($policy->{max_fine});
1834 $circ->fine_interval($recurring->recurrence_interval);
1835 $circ->renewal_remaining($duration->max_renewals);
1839 $logger->info("circulator: copy found with an unlimited circ duration");
1840 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1841 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1842 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1843 $circ->renewal_remaining(0);
1846 $circ->target_copy( $copy->id );
1847 $circ->usr( $patron->id );
1848 $circ->circ_lib( $self->circ_lib );
1849 $circ->workstation($self->editor->requestor->wsid)
1850 if defined $self->editor->requestor->wsid;
1852 # renewals maintain a link to the parent circulation
1853 $circ->parent_circ($self->parent_circ);
1855 if( $self->is_renewal ) {
1856 $circ->opac_renewal('t') if $self->opac_renewal;
1857 $circ->phone_renewal('t') if $self->phone_renewal;
1858 $circ->desk_renewal('t') if $self->desk_renewal;
1859 $circ->renewal_remaining($self->renewal_remaining);
1860 $circ->circ_staff($self->editor->requestor->id);
1864 # if the user provided an overiding checkout time,
1865 # (e.g. the checkout really happened several hours ago), then
1866 # we apply that here. Does this need a perm??
1867 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1868 if $self->checkout_time;
1870 # if a patron is renewing, 'requestor' will be the patron
1871 $circ->circ_staff($self->editor->requestor->id);
1872 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1877 sub do_reservation_pickup {
1880 $self->log_me("do_reservation_pickup()");
1882 $self->reservation->pickup_time('now');
1885 $self->reservation->current_resource &&
1886 $U->is_true($self->reservation->target_resource_type->catalog_item)
1888 # We used to try to set $self->copy and $self->patron here,
1889 # but that should already be done.
1891 $self->run_checkout_scripts(1);
1893 my $duration = $self->duration_rule;
1894 my $max = $self->max_fine_rule;
1895 my $recurring = $self->recurring_fines_rule;
1897 if ($duration && $max && $recurring) {
1898 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1900 my $dname = $duration->name;
1901 my $mname = $max->name;
1902 my $rname = $recurring->name;
1904 $logger->debug("circulator: updating reservation ".
1905 "with duration=$dname, maxfine=$mname, recurring=$rname");
1907 $self->reservation->fine_amount($policy->{recurring_fine});
1908 $self->reservation->max_fine($policy->{max_fine});
1909 $self->reservation->fine_interval($recurring->recurrence_interval);
1912 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1913 $self->update_copy();
1916 $self->reservation->fine_amount(
1917 $self->reservation->target_resource_type->fine_amount
1919 $self->reservation->max_fine(
1920 $self->reservation->target_resource_type->max_fine
1922 $self->reservation->fine_interval(
1923 $self->reservation->target_resource_type->fine_interval
1927 $self->update_reservation();
1930 sub do_reservation_return {
1932 my $request = shift;
1934 $self->log_me("do_reservation_return()");
1936 if (not ref $self->reservation) {
1937 my ($reservation, $evt) =
1938 $U->fetch_booking_reservation($self->reservation);
1939 return $self->bail_on_events($evt) if $evt;
1940 $self->reservation($reservation);
1943 $self->generate_fines(1);
1944 $self->reservation->return_time('now');
1945 $self->update_reservation();
1946 $self->reshelve_copy if $self->copy;
1948 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1949 $self->copy( $self->reservation->current_resource->catalog_item );
1953 sub booking_adjusted_due_date {
1955 my $circ = $self->circ;
1956 my $copy = $self->copy;
1958 return undef unless $self->use_booking;
1962 if( $self->due_date ) {
1964 return $self->bail_on_events($self->editor->event)
1965 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1967 $circ->due_date(cleanse_ISO8601($self->due_date));
1971 return unless $copy and $circ->due_date;
1974 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1975 if (@$booking_items) {
1976 my $booking_item = $booking_items->[0];
1977 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1979 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1980 my $shorten_circ_setting = $resource_type->elbow_room ||
1981 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1984 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1985 my $bookings = $booking_ses->request(
1986 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1987 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1989 $booking_ses->disconnect;
1991 my $dt_parser = DateTime::Format::ISO8601->new;
1992 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1994 for my $bid (@$bookings) {
1996 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1998 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1999 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2001 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2002 if ($booking_start < DateTime->now);
2005 if ($U->is_true($stop_circ_setting)) {
2006 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2008 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2009 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2012 # We set the circ duration here only to affect the logic that will
2013 # later (in a DB trigger) mangle the time part of the due date to
2014 # 11:59pm. Having any circ duration that is not a whole number of
2015 # days is enough to prevent the "correction."
2016 my $new_circ_duration = $due_date->epoch - time;
2017 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2018 $circ->duration("$new_circ_duration seconds");
2020 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2024 return $self->bail_on_events($self->editor->event)
2025 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2031 sub apply_modified_due_date {
2033 my $shift_earlier = shift;
2034 my $circ = $self->circ;
2035 my $copy = $self->copy;
2037 if( $self->due_date ) {
2039 return $self->bail_on_events($self->editor->event)
2040 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2042 $circ->due_date(cleanse_ISO8601($self->due_date));
2046 # if the due_date lands on a day when the location is closed
2047 return unless $copy and $circ->due_date;
2049 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2051 # due-date overlap should be determined by the location the item
2052 # is checked out from, not the owning or circ lib of the item
2053 my $org = $self->circ_lib;
2055 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2056 " with an item due date of ".$circ->due_date );
2058 my $dateinfo = $U->storagereq(
2059 'open-ils.storage.actor.org_unit.closed_date.overlap',
2060 $org, $circ->due_date );
2063 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2064 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2066 # XXX make the behavior more dynamic
2067 # for now, we just push the due date to after the close date
2068 if ($shift_earlier) {
2069 $circ->due_date($dateinfo->{start});
2071 $circ->due_date($dateinfo->{end});
2079 sub create_due_date {
2080 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2082 # if there is a raw time component (e.g. from postgres),
2083 # turn it into an interval that interval_to_seconds can parse
2084 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2086 # for now, use the server timezone. TODO: use workstation org timezone
2087 my $due_date = DateTime->now(time_zone => 'local');
2089 # add the circ duration
2090 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2093 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2094 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2095 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2100 # return ISO8601 time with timezone
2101 return $due_date->strftime('%FT%T%z');
2106 sub make_precat_copy {
2108 my $copy = $self->copy;
2111 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2113 $copy->editor($self->editor->requestor->id);
2114 $copy->edit_date('now');
2115 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2116 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2117 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2118 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2119 $self->update_copy();
2123 $logger->info("circulator: Creating a new precataloged ".
2124 "copy in checkout with barcode " . $self->copy_barcode);
2126 $copy = Fieldmapper::asset::copy->new;
2127 $copy->circ_lib($self->circ_lib);
2128 $copy->creator($self->editor->requestor->id);
2129 $copy->editor($self->editor->requestor->id);
2130 $copy->barcode($self->copy_barcode);
2131 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2132 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2133 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2135 $copy->dummy_title($self->dummy_title || "");
2136 $copy->dummy_author($self->dummy_author || "");
2137 $copy->dummy_isbn($self->dummy_isbn || "");
2138 $copy->circ_modifier($self->circ_modifier);
2141 # See if we need to override the circ_lib for the copy with a configured circ_lib
2142 # Setting is shortname of the org unit
2143 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2144 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2146 if($precat_circ_lib) {
2147 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2150 $self->bail_on_events($self->editor->event);
2154 $copy->circ_lib($org->id);
2158 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2160 $self->push_events($self->editor->event);
2164 # this is a little bit of a hack, but we need to
2165 # get the copy into the script runner
2166 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2170 sub checkout_noncat {
2176 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2177 my $count = $self->noncat_count || 1;
2178 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2180 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2184 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2185 $self->editor->requestor->id,
2193 $self->push_events($evt);
2204 $self->log_me("do_checkin()");
2206 return $self->bail_on_events(
2207 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2210 # the renew code and mk_env should have already found our circulation object
2211 unless( $self->circ ) {
2213 my $circs = $self->editor->search_action_circulation(
2214 { target_copy => $self->copy->id, checkin_time => undef });
2216 $self->circ($$circs[0]);
2218 # for now, just warn if there are multiple open circs on a copy
2219 $logger->warn("circulator: we have ".scalar(@$circs).
2220 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2223 # run the fine generator against this circ, if this circ is there
2224 $self->generate_fines_start if $self->circ;
2226 if( $self->checkin_check_holds_shelf() ) {
2227 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2228 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2229 $self->checkin_flesh_events;
2233 unless( $self->is_renewal ) {
2234 return $self->bail_on_events($self->editor->event)
2235 unless $self->editor->allowed('COPY_CHECKIN');
2238 $self->push_events($self->check_copy_alert());
2239 $self->push_events($self->check_checkin_copy_status());
2241 # if the circ is marked as 'claims returned', add the event to the list
2242 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2243 if ($self->circ and $self->circ->stop_fines
2244 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2246 $self->check_circ_deposit();
2248 # handle the overridable events
2249 $self->override_events unless $self->is_renewal;
2250 return if $self->bail_out;
2254 $self->editor->search_action_transit_copy(
2255 { target_copy => $self->copy->id, dest_recv_time => undef }
2261 $self->checkin_handle_circ;
2262 return if $self->bail_out;
2263 $self->checkin_changed(1);
2265 } elsif( $self->transit ) {
2266 my $hold_transit = $self->process_received_transit;
2267 $self->checkin_changed(1);
2269 if( $self->bail_out ) {
2270 $self->checkin_flesh_events;
2274 if( my $e = $self->check_checkin_copy_status() ) {
2275 # If the original copy status is special, alert the caller
2276 my $ev = $self->events;
2277 $self->events([$e]);
2278 $self->override_events;
2279 return if $self->bail_out;
2283 if( $hold_transit or
2284 $U->copy_status($self->copy->status)->id
2285 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2288 if( $hold_transit ) {
2289 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2291 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2296 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2298 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2299 $self->reshelve_copy(1);
2300 $self->cancelled_hold_transit(1);
2301 $self->notify_hold(0); # don't notify for cancelled holds
2302 return if $self->bail_out;
2306 # hold transited to correct location
2307 $self->checkin_flesh_events;
2312 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2314 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2315 " that is in-transit, but there is no transit.. repairing");
2316 $self->reshelve_copy(1);
2317 return if $self->bail_out;
2320 if( $self->is_renewal ) {
2321 $self->finish_fines_and_voiding;
2322 return if $self->bail_out;
2323 $self->push_events(OpenILS::Event->new('SUCCESS'));
2327 # ------------------------------------------------------------------------------
2328 # Circulations and transits are now closed where necessary. Now go on to see if
2329 # this copy can fulfill a hold or needs to be routed to a different location
2330 # ------------------------------------------------------------------------------
2332 my $needed_for_something = 0; # formerly "needed_for_hold"
2334 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2336 if (!$self->remote_hold) {
2337 if ($self->use_booking) {
2338 my $potential_hold = $self->hold_capture_is_possible;
2339 my $potential_reservation = $self->reservation_capture_is_possible;
2341 if ($potential_hold and $potential_reservation) {
2342 $logger->info("circulator: item could fulfill either hold or reservation");
2343 $self->push_events(new OpenILS::Event(
2344 "HOLD_RESERVATION_CONFLICT",
2345 "hold" => $potential_hold,
2346 "reservation" => $potential_reservation
2348 return if $self->bail_out;
2349 } elsif ($potential_hold) {
2350 $needed_for_something =
2351 $self->attempt_checkin_hold_capture;
2352 } elsif ($potential_reservation) {
2353 $needed_for_something =
2354 $self->attempt_checkin_reservation_capture;
2357 $needed_for_something = $self->attempt_checkin_hold_capture;
2360 return if $self->bail_out;
2362 unless($needed_for_something) {
2363 my $circ_lib = (ref $self->copy->circ_lib) ?
2364 $self->copy->circ_lib->id : $self->copy->circ_lib;
2366 if( $self->remote_hold ) {
2367 $circ_lib = $self->remote_hold->pickup_lib;
2368 $logger->warn("circulator: Copy ".$self->copy->barcode.
2369 " is on a remote hold's shelf, sending to $circ_lib");
2372 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2374 if( $circ_lib == $self->circ_lib) {
2375 # copy is where it needs to be, either for hold or reshelving
2377 $self->checkin_handle_precat();
2378 return if $self->bail_out;
2381 # copy needs to transit "home", or stick here if it's a floating copy
2383 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2384 $self->checkin_changed(1);
2385 $self->copy->circ_lib( $self->circ_lib );
2388 my $bc = $self->copy->barcode;
2389 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2390 $self->checkin_build_copy_transit($circ_lib);
2391 return if $self->bail_out;
2392 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2396 } else { # no-op checkin
2397 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2398 $self->checkin_changed(1);
2399 $self->copy->circ_lib( $self->circ_lib );
2404 if($self->claims_never_checked_out and
2405 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2407 # the item was not supposed to be checked out to the user and should now be marked as missing
2408 $self->copy->status(OILS_COPY_STATUS_MISSING);
2412 $self->reshelve_copy unless $needed_for_something;
2415 return if $self->bail_out;
2417 unless($self->checkin_changed) {
2419 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2420 my $stat = $U->copy_status($self->copy->status)->id;
2422 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2423 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2424 $self->bail_out(1); # no need to commit anything
2428 $self->push_events(OpenILS::Event->new('SUCCESS'))
2429 unless @{$self->events};
2432 $self->finish_fines_and_voiding;
2434 OpenILS::Utils::Penalty->calculate_penalties(
2435 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2437 $self->checkin_flesh_events;
2441 sub finish_fines_and_voiding {
2443 return unless $self->circ;
2445 # gather any updates to the circ after fine generation, if there was a circ
2446 $self->generate_fines_finish;
2448 return unless $self->backdate or $self->void_overdues;
2450 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2451 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2453 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2454 $self->editor, $self->circ, $self->backdate, $note);
2456 return $self->bail_on_events($evt) if $evt;
2458 # make sure the circ isn't closed if we just voided some fines
2459 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2460 return $self->bail_on_events($evt) if $evt;
2466 # if a deposit was payed for this item, push the event
2467 sub check_circ_deposit {
2469 return unless $self->circ;
2470 my $deposit = $self->editor->search_money_billing(
2472 xact => $self->circ->id,
2474 }, {idlist => 1})->[0];
2476 $self->push_events(OpenILS::Event->new(
2477 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2482 my $force = $self->force || shift;
2483 my $copy = $self->copy;
2485 my $stat = $U->copy_status($copy->status)->id;
2488 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2489 $stat != OILS_COPY_STATUS_CATALOGING and
2490 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2491 $stat != OILS_COPY_STATUS_RESHELVING )) {
2493 $copy->status( OILS_COPY_STATUS_RESHELVING );
2495 $self->checkin_changed(1);
2500 # Returns true if the item is at the current location
2501 # because it was transited there for a hold and the
2502 # hold has not been fulfilled
2503 sub checkin_check_holds_shelf {
2505 return 0 unless $self->copy;
2508 $U->copy_status($self->copy->status)->id ==
2509 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2511 # find the hold that put us on the holds shelf
2512 my $holds = $self->editor->search_action_hold_request(
2514 current_copy => $self->copy->id,
2515 capture_time => { '!=' => undef },
2516 fulfillment_time => undef,
2517 cancel_time => undef,
2522 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2523 $self->reshelve_copy(1);
2527 my $hold = $$holds[0];
2529 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2530 $hold->id. "] for copy ".$self->copy->barcode);
2532 if( $hold->pickup_lib == $self->circ_lib ) {
2533 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2537 $logger->info("circulator: hold is not for here..");
2538 $self->remote_hold($hold);
2543 sub checkin_handle_precat {
2545 my $copy = $self->copy;
2547 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2548 $copy->status(OILS_COPY_STATUS_CATALOGING);
2549 $self->update_copy();
2550 $self->checkin_changed(1);
2551 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2556 sub checkin_build_copy_transit {
2559 my $copy = $self->copy;
2560 my $transit = Fieldmapper::action::transit_copy->new;
2562 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2563 $logger->info("circulator: transiting copy to $dest");
2565 $transit->source($self->circ_lib);
2566 $transit->dest($dest);
2567 $transit->target_copy($copy->id);
2568 $transit->source_send_time('now');
2569 $transit->copy_status( $U->copy_status($copy->status)->id );
2571 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2573 return $self->bail_on_events($self->editor->event)
2574 unless $self->editor->create_action_transit_copy($transit);
2576 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2578 $self->checkin_changed(1);
2582 sub hold_capture_is_possible {
2584 my $copy = $self->copy;
2586 # we've been explicitly told not to capture any holds
2587 return 0 if $self->capture eq 'nocapture';
2589 # See if this copy can fulfill any holds
2590 my $hold = $holdcode->find_nearest_permitted_hold(
2591 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2593 return undef if ref $hold eq "HASH" and
2594 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2598 sub reservation_capture_is_possible {
2600 my $copy = $self->copy;
2602 # we've been explicitly told not to capture any holds
2603 return 0 if $self->capture eq 'nocapture';
2605 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2606 my $resv = $booking_ses->request(
2607 "open-ils.booking.reservations.could_capture",
2608 $self->editor->authtoken, $copy->barcode
2610 $booking_ses->disconnect;
2611 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2612 $self->push_events($resv);
2618 # returns true if the item was used (or may potentially be used
2619 # in subsequent calls) to capture a hold.
2620 sub attempt_checkin_hold_capture {
2622 my $copy = $self->copy;
2624 # we've been explicitly told not to capture any holds
2625 return 0 if $self->capture eq 'nocapture';
2627 # See if this copy can fulfill any holds
2628 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2629 $self->editor, $copy, $self->editor->requestor );
2632 $logger->debug("circulator: no potential permitted".
2633 "holds found for copy ".$copy->barcode);
2637 if($self->capture ne 'capture') {
2638 # see if this item is in a hold-capture-delay location
2639 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2640 if($U->is_true($location->hold_verify)) {
2641 $self->bail_on_events(
2642 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2647 $self->retarget($retarget);
2649 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2651 $hold->current_copy($copy->id);
2652 $hold->capture_time('now');
2653 $self->put_hold_on_shelf($hold)
2654 if $hold->pickup_lib == $self->circ_lib;
2656 # prevent DB errors caused by fetching
2657 # holds from storage, and updating through cstore
2658 $hold->clear_fulfillment_time;
2659 $hold->clear_fulfillment_staff;
2660 $hold->clear_fulfillment_lib;
2661 $hold->clear_expire_time;
2662 $hold->clear_cancel_time;
2663 $hold->clear_prev_check_time unless $hold->prev_check_time;
2665 $self->bail_on_events($self->editor->event)
2666 unless $self->editor->update_action_hold_request($hold);
2668 $self->checkin_changed(1);
2670 return 0 if $self->bail_out;
2672 if( $hold->pickup_lib == $self->circ_lib ) {
2674 # This hold was captured in the correct location
2675 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2676 $self->push_events(OpenILS::Event->new('SUCCESS'));
2678 #$self->do_hold_notify($hold->id);
2679 $self->notify_hold($hold->id);
2683 # Hold needs to be picked up elsewhere. Build a hold
2684 # transit and route the item.
2685 $self->checkin_build_hold_transit();
2686 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2687 return 0 if $self->bail_out;
2688 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2691 # make sure we save the copy status
2696 sub attempt_checkin_reservation_capture {
2698 my $copy = $self->copy;
2700 # we've been explicitly told not to capture any holds
2701 return 0 if $self->capture eq 'nocapture';
2703 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2704 my $evt = $booking_ses->request(
2705 "open-ils.booking.resources.capture_for_reservation",
2706 $self->editor->authtoken,
2708 1 # don't update copy - we probably have it locked
2710 $booking_ses->disconnect;
2712 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2714 "open-ils.booking.resources.capture_for_reservation " .
2715 "didn't return an event!"
2719 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2720 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2722 # not-transferable is an error event we'll pass on the user
2723 $logger->warn("reservation capture attempted against non-transferable item");
2724 $self->push_events($evt);
2726 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2727 # Re-retrieve copy as reservation capture may have changed
2728 # its status and whatnot.
2730 "circulator: booking capture win on copy " . $self->copy->id
2732 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2734 "circulator: changing copy " . $self->copy->id .
2735 "'s status from " . $self->copy->status . " to " .
2738 $self->copy->status($new_copy_status);
2741 $self->reservation($evt->{"payload"}->{"reservation"});
2743 if (exists $evt->{"payload"}->{"transit"}) {
2747 "org" => $evt->{"payload"}->{"transit"}->dest
2751 $self->checkin_changed(1);
2755 # other results are treated as "nothing to capture"
2759 sub do_hold_notify {
2760 my( $self, $holdid ) = @_;
2762 my $e = new_editor(xact => 1);
2763 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2765 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2766 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2768 $logger->info("circulator: running delayed hold notify process");
2770 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2771 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2773 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2774 hold_id => $holdid, requestor => $self->editor->requestor);
2776 $logger->debug("circulator: built hold notifier");
2778 if(!$notifier->event) {
2780 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2782 my $stat = $notifier->send_email_notify;
2783 if( $stat == '1' ) {
2784 $logger->info("circulator: hold notify succeeded for hold $holdid");
2788 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2791 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2795 sub retarget_holds {
2797 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2798 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2799 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2800 # no reason to wait for the return value
2804 sub checkin_build_hold_transit {
2807 my $copy = $self->copy;
2808 my $hold = $self->hold;
2809 my $trans = Fieldmapper::action::hold_transit_copy->new;
2811 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2813 $trans->hold($hold->id);
2814 $trans->source($self->circ_lib);
2815 $trans->dest($hold->pickup_lib);
2816 $trans->source_send_time("now");
2817 $trans->target_copy($copy->id);
2819 # when the copy gets to its destination, it will recover
2820 # this status - put it onto the holds shelf
2821 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2823 return $self->bail_on_events($self->editor->event)
2824 unless $self->editor->create_action_hold_transit_copy($trans);
2829 sub process_received_transit {
2831 my $copy = $self->copy;
2832 my $copyid = $self->copy->id;
2834 my $status_name = $U->copy_status($copy->status)->name;
2835 $logger->debug("circulator: attempting transit receive on ".
2836 "copy $copyid. Copy status is $status_name");
2838 my $transit = $self->transit;
2840 if( $transit->dest != $self->circ_lib ) {
2841 # - this item is in-transit to a different location
2843 my $tid = $transit->id;
2844 my $loc = $self->circ_lib;
2845 my $dest = $transit->dest;
2847 $logger->info("circulator: Fowarding transit on copy which is destined ".
2848 "for a different location. transit=$tid, copy=$copyid, current ".
2849 "location=$loc, destination location=$dest");
2851 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2853 # grab the associated hold object if available
2854 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2855 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2857 return $self->bail_on_events($evt);
2860 # The transit is received, set the receive time
2861 $transit->dest_recv_time('now');
2862 $self->bail_on_events($self->editor->event)
2863 unless $self->editor->update_action_transit_copy($transit);
2865 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2867 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2868 $copy->status( $transit->copy_status );
2869 $self->update_copy();
2870 return if $self->bail_out;
2874 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2876 # hold has arrived at destination, set shelf time
2877 $self->put_hold_on_shelf($hold);
2878 $self->bail_on_events($self->editor->event)
2879 unless $self->editor->update_action_hold_request($hold);
2880 return if $self->bail_out;
2882 $self->notify_hold($hold_transit->hold);
2887 OpenILS::Event->new(
2890 payload => { transit => $transit, holdtransit => $hold_transit } ));
2892 return $hold_transit;
2896 # ------------------------------------------------------------------
2897 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2898 # ------------------------------------------------------------------
2899 sub put_hold_on_shelf {
2900 my($self, $hold) = @_;
2902 $hold->shelf_time('now');
2904 my $shelf_expire = $U->ou_ancestor_setting_value(
2905 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2908 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2909 my $expire_time = DateTime->now->add(seconds => $seconds);
2910 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2918 sub generate_fines {
2920 my $reservation = shift;
2922 $self->generate_fines_start($reservation);
2923 $self->generate_fines_finish($reservation);
2928 sub generate_fines_start {
2930 my $reservation = shift;
2932 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2934 if (!exists($self->{_gen_fines_req})) {
2935 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
2937 'open-ils.storage.action.circulation.overdue.generate_fines',
2946 sub generate_fines_finish {
2948 my $reservation = shift;
2950 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2952 $self->{_gen_fines_req}->wait_complete;
2953 delete($self->{_gen_fines_req});
2955 # refresh the circ in case the fine generator set the stop_fines field
2956 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2957 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2962 sub checkin_handle_circ {
2964 my $circ = $self->circ;
2965 my $copy = $self->copy;
2969 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2971 # backdate the circ if necessary
2972 if($self->backdate) {
2973 my $evt = $self->checkin_handle_backdate;
2974 return $self->bail_on_events($evt) if $evt;
2977 if(!$circ->stop_fines) {
2978 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2979 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2980 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2981 $circ->stop_fines_time('now');
2982 $circ->stop_fines_time($self->backdate) if $self->backdate;
2985 # Set the checkin vars since we have the item
2986 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2988 # capture the true scan time for back-dated checkins
2989 $circ->checkin_scan_time('now');
2991 $circ->checkin_staff($self->editor->requestor->id);
2992 $circ->checkin_lib($self->circ_lib);
2993 $circ->checkin_workstation($self->editor->requestor->wsid);
2995 my $circ_lib = (ref $self->copy->circ_lib) ?
2996 $self->copy->circ_lib->id : $self->copy->circ_lib;
2997 my $stat = $U->copy_status($self->copy->status)->id;
2999 # immediately available keeps items lost or missing items from going home before being handled
3000 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3001 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3004 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3006 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3007 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3009 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3013 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3015 $self->checkin_handle_lost($circ_lib);
3019 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3024 # see if there are any fines owed on this circ. if not, close it
3025 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3026 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3028 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3030 return $self->bail_on_events($self->editor->event)
3031 unless $self->editor->update_action_circulation($circ);
3037 # ------------------------------------------------------------------
3038 # See if we need to void billings for lost checkin
3039 # ------------------------------------------------------------------
3040 sub checkin_handle_lost {
3042 my $circ_lib = shift;
3043 my $circ = $self->circ;
3045 my $max_return = $U->ou_ancestor_setting_value(
3046 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3051 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3052 $tm[5] -= 1 if $tm[5] > 0;
3053 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3055 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3056 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3058 $max_return = 0 if $today < $last_chance;
3061 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3063 my $void_lost = $U->ou_ancestor_setting_value(
3064 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3065 my $void_lost_fee = $U->ou_ancestor_setting_value(
3066 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3067 my $restore_od = $U->ou_ancestor_setting_value(
3068 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3070 $self->checkin_handle_lost_now_found(3) if $void_lost;
3071 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3072 $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3075 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3080 sub checkin_handle_backdate {
3083 # ------------------------------------------------------------------
3084 # clean up the backdate for date comparison
3085 # XXX We are currently taking the due-time from the original due-date,
3086 # not the input. Do we need to do this? This certainly interferes with
3087 # backdating of hourly checkouts, but that is likely a very rare case.
3088 # ------------------------------------------------------------------
3089 my $bd = cleanse_ISO8601($self->backdate);
3090 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3091 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3092 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3094 $self->backdate($bd);
3099 sub check_checkin_copy_status {
3101 my $copy = $self->copy;
3103 my $status = $U->copy_status($copy->status)->id;
3106 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3107 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3108 $status == OILS_COPY_STATUS_IN_PROCESS ||
3109 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3110 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3111 $status == OILS_COPY_STATUS_CATALOGING ||
3112 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3113 $status == OILS_COPY_STATUS_RESHELVING );
3115 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3116 if( $status == OILS_COPY_STATUS_LOST );
3118 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3119 if( $status == OILS_COPY_STATUS_MISSING );
3121 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3126 # --------------------------------------------------------------------------
3127 # On checkin, we need to return as many relevant objects as we can
3128 # --------------------------------------------------------------------------
3129 sub checkin_flesh_events {
3132 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3133 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3134 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3137 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3140 if($self->hold and !$self->hold->cancel_time) {
3141 $hold = $self->hold;
3142 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3146 # if we checked in a circulation, flesh the billing summary data
3147 $self->circ->billable_transaction(
3148 $self->editor->retrieve_money_billable_transaction([
3150 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3156 # flesh some patron fields before returning
3158 $self->editor->retrieve_actor_user([
3163 au => ['card', 'billing_address', 'mailing_address']
3170 for my $evt (@{$self->events}) {
3173 $payload->{copy} = $U->unflesh_copy($self->copy);
3174 $payload->{record} = $record,
3175 $payload->{circ} = $self->circ;
3176 $payload->{transit} = $self->transit;
3177 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3178 $payload->{hold} = $hold;
3179 $payload->{patron} = $self->patron;
3180 $payload->{reservation} = $self->reservation
3181 unless (not $self->reservation or $self->reservation->cancel_time);
3183 $evt->{payload} = $payload;
3188 my( $self, $msg ) = @_;
3189 my $bc = ($self->copy) ? $self->copy->barcode :
3192 my $usr = ($self->patron) ? $self->patron->id : "";
3193 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3194 ", recipient=$usr, copy=$bc");
3200 $self->log_me("do_renew()");
3202 # Make sure there is an open circ to renew that is not
3203 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3204 my $usrid = $self->patron->id if $self->patron;
3205 my $circ = $self->editor->search_action_circulation({
3206 target_copy => $self->copy->id,
3207 xact_finish => undef,
3208 ($usrid ? (usr => $usrid) : ()),
3210 {stop_fines => undef},
3211 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3215 return $self->bail_on_events($self->editor->event) unless $circ;
3217 # A user is not allowed to renew another user's items without permission
3218 unless( $circ->usr eq $self->editor->requestor->id ) {
3219 return $self->bail_on_events($self->editor->events)
3220 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3223 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3224 if $circ->renewal_remaining < 1;
3226 # -----------------------------------------------------------------
3228 $self->parent_circ($circ->id);
3229 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3232 # Run the fine generator against the old circ
3233 $self->generate_fines_start;
3235 $self->run_renew_permit;
3238 $self->do_checkin();
3239 return if $self->bail_out;
3241 unless( $self->permit_override ) {
3243 return if $self->bail_out;
3244 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3245 $self->remove_event('ITEM_NOT_CATALOGED');
3248 $self->override_events;
3249 return if $self->bail_out;
3252 $self->do_checkout();
3257 my( $self, $evt ) = @_;
3258 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3259 $logger->debug("circulator: removing event from list: $evt");
3260 my @events = @{$self->events};
3261 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3266 my( $self, $evt ) = @_;
3267 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3268 return grep { $_->{textcode} eq $evt } @{$self->events};
3273 sub run_renew_permit {
3276 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3277 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3278 $self->editor, $self->copy, $self->editor->requestor, 1
3280 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3283 if(!$self->legacy_script_support) {
3284 my $results = $self->run_indb_circ_test;
3285 $self->push_events($self->matrix_test_result_events)
3286 unless $self->circ_test_success;
3289 my $runner = $self->script_runner;
3291 $runner->load($self->circ_permit_renew);
3292 my $result = $runner->run or
3293 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3294 if ($result->{"events"}) {
3296 map { new OpenILS::Event($_) } @{$result->{"events"}}
3299 "circulator: circ_permit_renew for user " .
3300 $self->patron->id . " returned " .
3301 scalar(@{$result->{"events"}}) . " event(s)"
3305 $self->mk_script_runner;
3308 $logger->debug("circulator: re-creating script runner to be safe");
3312 # XXX: The primary mechanism for storing circ history is now handled
3313 # by tracking real circulation objects instead of bibs in a bucket.
3314 # However, this code is disabled by default and could be useful
3315 # some day, so may as well leave it for now.
3316 sub append_reading_list {
3320 $self->is_checkout and
3326 # verify history is globally enabled and uses the bucket mechanism
3327 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3328 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3330 return undef unless $htype and $htype eq 'bucket';
3332 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3334 # verify the patron wants to retain the hisory
3335 my $setting = $e->search_actor_user_setting(
3336 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3338 unless($setting and $setting->value) {
3343 my $bkt = $e->search_container_copy_bucket(
3344 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3349 # find the next item position
3350 my $last_item = $e->search_container_copy_bucket_item(
3351 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3352 $pos = $last_item->pos + 1 if $last_item;
3355 # create the history bucket if necessary
3356 $bkt = Fieldmapper::container::copy_bucket->new;
3357 $bkt->owner($self->patron->id);
3359 $bkt->btype('circ_history');
3361 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3364 my $item = Fieldmapper::container::copy_bucket_item->new;
3366 $item->bucket($bkt->id);
3367 $item->target_copy($self->copy->id);
3370 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3377 sub make_trigger_events {
3379 return unless $self->circ;
3380 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3381 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3382 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3387 sub checkin_handle_lost_now_found {
3388 my ($self, $bill_type) = @_;
3390 # ------------------------------------------------------------------
3391 # remove charge from patron's account if lost item is returned
3392 # ------------------------------------------------------------------
3394 my $bills = $self->editor->search_money_billing(
3396 xact => $self->circ->id,
3401 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3402 for my $bill (@$bills) {
3403 if( !$U->is_true($bill->voided) ) {
3404 $logger->info("lost item returned - voiding bill ".$bill->id);
3406 $bill->void_time('now');
3407 $bill->voider($self->editor->requestor->id);
3408 my $note = ($bill->note) ? $bill->note . "\n" : '';
3409 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3411 $self->bail_on_events($self->editor->event)
3412 unless $self->editor->update_money_billing($bill);
3417 sub checkin_handle_lost_now_found_restore_od {
3420 # ------------------------------------------------------------------
3421 # restore those overdue charges voided when item was set to lost
3422 # ------------------------------------------------------------------
3424 my $ods = $self->editor->search_money_billing(
3426 xact => $self->circ->id,
3431 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3432 for my $bill (@$ods) {
3433 if( $U->is_true($bill->voided) ) {
3434 $logger->info("lost item returned - restoring overdue ".$bill->id);
3436 $bill->clear_void_time;
3437 $bill->voider($self->editor->requestor->id);
3438 my $note = ($bill->note) ? $bill->note . "\n" : '';
3439 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3441 $self->bail_on_events($self->editor->event)
3442 unless $self->editor->update_money_billing($bill);