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 if($results->[0]->{grace_period}) {
1166 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1168 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1169 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1172 return $self->matrix_test_result($results);
1175 # ---------------------------------------------------------------------
1176 # given a use and copy, this will calculate the circulation policy
1177 # parameters. Only works with in-db circ.
1178 # ---------------------------------------------------------------------
1182 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1184 $self->run_indb_circ_test;
1187 circ_test_success => $self->circ_test_success,
1188 failure_events => [],
1189 failure_codes => [],
1190 matchpoint => $self->circ_matrix_matchpoint
1193 unless($self->circ_test_success) {
1194 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1195 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1198 if($self->circ_matrix_matchpoint) {
1199 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1200 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1201 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1202 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1204 my $policy = $self->get_circ_policy(
1205 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1207 $$results{$_} = $$policy{$_} for keys %$policy;
1213 # ---------------------------------------------------------------------
1214 # Loads the circ policy info for duration, recurring fine, and max
1215 # fine based on the current copy
1216 # ---------------------------------------------------------------------
1217 sub get_circ_policy {
1218 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1221 duration_rule => $duration_rule->name,
1222 recurring_fine_rule => $recurring_fine_rule->name,
1223 max_fine_rule => $max_fine_rule->name,
1224 max_fine => $self->get_max_fine_amount($max_fine_rule),
1225 fine_interval => $recurring_fine_rule->recurrence_interval,
1226 renewal_remaining => $duration_rule->max_renewals,
1227 grace_period => $recurring_fine_rule->grace_period
1230 if($hard_due_date) {
1231 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1232 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1235 $policy->{duration_date_ceiling} = undef;
1236 $policy->{duration_date_ceiling_force} = undef;
1239 $policy->{duration} = $duration_rule->shrt
1240 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1241 $policy->{duration} = $duration_rule->normal
1242 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1243 $policy->{duration} = $duration_rule->extended
1244 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1246 $policy->{recurring_fine} = $recurring_fine_rule->low
1247 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1248 $policy->{recurring_fine} = $recurring_fine_rule->normal
1249 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1250 $policy->{recurring_fine} = $recurring_fine_rule->high
1251 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1256 sub get_max_fine_amount {
1258 my $max_fine_rule = shift;
1259 my $max_amount = $max_fine_rule->amount;
1261 # if is_percent is true then the max->amount is
1262 # use as a percentage of the copy price
1263 if ($U->is_true($max_fine_rule->is_percent)) {
1264 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1265 $max_amount = $price * $max_fine_rule->amount / 100;
1267 $U->ou_ancestor_setting_value(
1269 'circ.max_fine.cap_at_price',
1273 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1274 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1282 sub run_copy_permit_scripts {
1284 my $copy = $self->copy || return;
1285 my $runner = $self->script_runner;
1289 if(!$self->legacy_script_support) {
1290 my $results = $self->run_indb_circ_test;
1291 push @allevents, $self->matrix_test_result_events
1292 unless $self->circ_test_success;
1295 # ---------------------------------------------------------------------
1296 # Capture all of the copy permit events
1297 # ---------------------------------------------------------------------
1298 $runner->load($self->circ_permit_copy);
1299 my $result = $runner->run or
1300 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1301 my $copy_events = $result->{events};
1303 # ---------------------------------------------------------------------
1304 # Now collect all of the events together
1305 # ---------------------------------------------------------------------
1306 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1309 # See if this copy has an alert message
1310 my $ae = $self->check_copy_alert();
1311 push( @allevents, $ae ) if $ae;
1313 # uniquify the events
1314 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1315 @allevents = values %hash;
1317 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1319 $self->push_events(@allevents);
1323 sub check_copy_alert {
1325 return undef if $self->is_renewal;
1326 return OpenILS::Event->new(
1327 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1328 if $self->copy and $self->copy->alert_message;
1334 # --------------------------------------------------------------------------
1335 # If the call is overriding and has permissions to override every collected
1336 # event, the are cleared. Any event that the caller does not have
1337 # permission to override, will be left in the event list and bail_out will
1339 # XXX We need code in here to cancel any holds/transits on copies
1340 # that are being force-checked out
1341 # --------------------------------------------------------------------------
1342 sub override_events {
1344 my @events = @{$self->events};
1345 return unless @events;
1347 if(!$self->override) {
1348 return $self->bail_out(1)
1349 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1354 for my $e (@events) {
1355 my $tc = $e->{textcode};
1356 next if $tc eq 'SUCCESS';
1357 my $ov = "$tc.override";
1358 $logger->info("circulator: attempting to override event: $ov");
1360 return $self->bail_on_events($self->editor->event)
1361 unless( $self->editor->allowed($ov) );
1366 # --------------------------------------------------------------------------
1367 # If there is an open claimsreturn circ on the requested copy, close the
1368 # circ if overriding, otherwise bail out
1369 # --------------------------------------------------------------------------
1370 sub handle_claims_returned {
1372 my $copy = $self->copy;
1374 my $CR = $self->editor->search_action_circulation(
1376 target_copy => $copy->id,
1377 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1378 checkin_time => undef,
1382 return unless ($CR = $CR->[0]);
1386 # - If the caller has set the override flag, we will check the item in
1387 if($self->override) {
1389 $CR->checkin_time('now');
1390 $CR->checkin_scan_time('now');
1391 $CR->checkin_lib($self->circ_lib);
1392 $CR->checkin_workstation($self->editor->requestor->wsid);
1393 $CR->checkin_staff($self->editor->requestor->id);
1395 $evt = $self->editor->event
1396 unless $self->editor->update_action_circulation($CR);
1399 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1402 $self->bail_on_events($evt) if $evt;
1407 # --------------------------------------------------------------------------
1408 # This performs the checkout
1409 # --------------------------------------------------------------------------
1413 $self->log_me("do_checkout()");
1415 # make sure perms are good if this isn't a renewal
1416 unless( $self->is_renewal ) {
1417 return $self->bail_on_events($self->editor->event)
1418 unless( $self->editor->allowed('COPY_CHECKOUT') );
1421 # verify the permit key
1422 unless( $self->check_permit_key ) {
1423 if( $self->permit_override ) {
1424 return $self->bail_on_events($self->editor->event)
1425 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1427 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1431 # if this is a non-cataloged circ, build the circ and finish
1432 if( $self->is_noncat ) {
1433 $self->checkout_noncat;
1435 OpenILS::Event->new('SUCCESS',
1436 payload => { noncat_circ => $self->circ }));
1440 if( $self->is_precat ) {
1441 $self->make_precat_copy;
1442 return if $self->bail_out;
1444 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1445 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1448 $self->do_copy_checks;
1449 return if $self->bail_out;
1451 $self->run_checkout_scripts();
1452 return if $self->bail_out;
1454 $self->build_checkout_circ_object();
1455 return if $self->bail_out;
1457 my $modify_to_start = $self->booking_adjusted_due_date();
1458 return if $self->bail_out;
1460 $self->apply_modified_due_date($modify_to_start);
1461 return if $self->bail_out;
1463 return $self->bail_on_events($self->editor->event)
1464 unless $self->editor->create_action_circulation($self->circ);
1466 # refresh the circ to force local time zone for now
1467 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1469 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1471 return if $self->bail_out;
1473 $self->apply_deposit_fee();
1474 return if $self->bail_out;
1476 $self->handle_checkout_holds();
1477 return if $self->bail_out;
1479 # ------------------------------------------------------------------------------
1480 # Update the patron penalty info in the DB. Run it for permit-overrides
1481 # since the penalties are not updated during the permit phase
1482 # ------------------------------------------------------------------------------
1483 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1485 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1488 if($self->is_renewal) {
1489 # flesh the billing summary for the checked-in circ
1490 $pcirc = $self->editor->retrieve_action_circulation([
1492 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1497 OpenILS::Event->new('SUCCESS',
1499 copy => $U->unflesh_copy($self->copy),
1500 circ => $self->circ,
1502 holds_fulfilled => $self->fulfilled_holds,
1503 deposit_billing => $self->deposit_billing,
1504 rental_billing => $self->rental_billing,
1505 parent_circ => $pcirc,
1506 patron => ($self->return_patron) ? $self->patron : undef,
1507 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1513 sub apply_deposit_fee {
1515 my $copy = $self->copy;
1517 ($self->is_deposit and not $self->is_deposit_exempt) or
1518 ($self->is_rental and not $self->is_rental_exempt);
1520 return if $self->is_deposit and $self->skip_deposit_fee;
1521 return if $self->is_rental and $self->skip_rental_fee;
1523 my $bill = Fieldmapper::money::billing->new;
1524 my $amount = $copy->deposit_amount;
1528 if($self->is_deposit) {
1529 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1531 $self->deposit_billing($bill);
1533 $billing_type = OILS_BILLING_TYPE_RENTAL;
1535 $self->rental_billing($bill);
1538 $bill->xact($self->circ->id);
1539 $bill->amount($amount);
1540 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1541 $bill->billing_type($billing_type);
1542 $bill->btype($btype);
1543 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1545 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1550 my $copy = $self->copy;
1552 my $stat = $copy->status if ref $copy->status;
1553 my $loc = $copy->location if ref $copy->location;
1554 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1556 $copy->status($stat->id) if $stat;
1557 $copy->location($loc->id) if $loc;
1558 $copy->circ_lib($circ_lib->id) if $circ_lib;
1559 $copy->editor($self->editor->requestor->id);
1560 $copy->edit_date('now');
1561 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1563 return $self->bail_on_events($self->editor->event)
1564 unless $self->editor->update_asset_copy($self->copy);
1566 $copy->status($U->copy_status($copy->status));
1567 $copy->location($loc) if $loc;
1568 $copy->circ_lib($circ_lib) if $circ_lib;
1571 sub update_reservation {
1573 my $reservation = $self->reservation;
1575 my $usr = $reservation->usr;
1576 my $target_rt = $reservation->target_resource_type;
1577 my $target_r = $reservation->target_resource;
1578 my $current_r = $reservation->current_resource;
1580 $reservation->usr($usr->id) if ref $usr;
1581 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1582 $reservation->target_resource($target_r->id) if ref $target_r;
1583 $reservation->current_resource($current_r->id) if ref $current_r;
1585 return $self->bail_on_events($self->editor->event)
1586 unless $self->editor->update_booking_reservation($self->reservation);
1589 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1590 $self->reservation($reservation);
1594 sub bail_on_events {
1595 my( $self, @evts ) = @_;
1596 $self->push_events(@evts);
1601 # ------------------------------------------------------------------------------
1602 # When an item is checked out, see if we can fulfill a hold for this patron
1603 # ------------------------------------------------------------------------------
1604 sub handle_checkout_holds {
1606 my $copy = $self->copy;
1607 my $patron = $self->patron;
1609 my $e = $self->editor;
1610 $self->fulfilled_holds([]);
1612 # pre/non-cats can't fulfill a hold
1613 return if $self->is_precat or $self->is_noncat;
1615 my $hold = $e->search_action_hold_request({
1616 current_copy => $copy->id ,
1617 cancel_time => undef,
1618 fulfillment_time => undef,
1620 {expire_time => undef},
1621 {expire_time => {'>' => 'now'}}
1625 if($hold and $hold->usr != $patron->id) {
1626 # reset the hold since the copy is now checked out
1628 $logger->info("circulator: un-targeting hold ".$hold->id.
1629 " because copy ".$copy->id." is getting checked out");
1631 $hold->clear_prev_check_time;
1632 $hold->clear_current_copy;
1633 $hold->clear_capture_time;
1635 return $self->bail_on_event($e->event)
1636 unless $e->update_action_hold_request($hold);
1642 $hold = $self->find_related_user_hold($copy, $patron) or return;
1643 $logger->info("circulator: found related hold to fulfill in checkout");
1646 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1648 # if the hold was never officially captured, capture it.
1649 $hold->current_copy($copy->id);
1650 $hold->capture_time('now') unless $hold->capture_time;
1651 $hold->fulfillment_time('now');
1652 $hold->fulfillment_staff($e->requestor->id);
1653 $hold->fulfillment_lib($self->circ_lib);
1655 return $self->bail_on_events($e->event)
1656 unless $e->update_action_hold_request($hold);
1658 $holdcode->delete_hold_copy_maps($e, $hold->id);
1659 return $self->fulfilled_holds([$hold->id]);
1663 # ------------------------------------------------------------------------------
1664 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1665 # the patron directly targets the checked out item, see if there is another hold
1666 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1667 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1668 # ------------------------------------------------------------------------------
1669 sub find_related_user_hold {
1670 my($self, $copy, $patron) = @_;
1671 my $e = $self->editor;
1673 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1675 return undef unless $U->ou_ancestor_setting_value(
1676 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1678 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1680 select => {ahr => ['id']},
1685 fkey => 'current_copy',
1686 type => 'left' # there may be no current_copy
1693 fulfillment_time => undef,
1694 cancel_time => undef,
1696 {expire_time => undef},
1697 {expire_time => {'>' => 'now'}}
1704 target => $self->volume->id
1710 target => $self->title->id
1716 {id => undef}, # left-join copy may be nonexistent
1717 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1721 order_by => {ahr => {request_time => {direction => 'asc'}}},
1725 my $hold_info = $e->json_query($args)->[0];
1726 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1731 sub run_checkout_scripts {
1736 my $runner = $self->script_runner;
1745 my $hard_due_date_name;
1747 if(!$self->legacy_script_support) {
1748 $self->run_indb_circ_test();
1749 $duration = $self->circ_matrix_matchpoint->duration_rule;
1750 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1751 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1752 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1756 $runner->load($self->circ_duration);
1758 my $result = $runner->run or
1759 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1761 $duration_name = $result->{durationRule};
1762 $recurring_name = $result->{recurringFinesRule};
1763 $max_fine_name = $result->{maxFine};
1764 $hard_due_date_name = $result->{hardDueDate};
1767 $duration_name = $duration->name if $duration;
1768 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1771 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1772 return $self->bail_on_events($evt) if ($evt && !$nobail);
1774 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1775 return $self->bail_on_events($evt) if ($evt && !$nobail);
1777 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1778 return $self->bail_on_events($evt) if ($evt && !$nobail);
1780 if($hard_due_date_name) {
1781 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1782 return $self->bail_on_events($evt) if ($evt && !$nobail);
1788 # The item circulates with an unlimited duration
1792 $hard_due_date = undef;
1795 $self->duration_rule($duration);
1796 $self->recurring_fines_rule($recurring);
1797 $self->max_fine_rule($max_fine);
1798 $self->hard_due_date($hard_due_date);
1802 sub build_checkout_circ_object {
1805 my $circ = Fieldmapper::action::circulation->new;
1806 my $duration = $self->duration_rule;
1807 my $max = $self->max_fine_rule;
1808 my $recurring = $self->recurring_fines_rule;
1809 my $hard_due_date = $self->hard_due_date;
1810 my $copy = $self->copy;
1811 my $patron = $self->patron;
1812 my $duration_date_ceiling;
1813 my $duration_date_ceiling_force;
1817 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1818 $duration_date_ceiling = $policy->{duration_date_ceiling};
1819 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1821 my $dname = $duration->name;
1822 my $mname = $max->name;
1823 my $rname = $recurring->name;
1825 if($hard_due_date) {
1826 $hdname = $hard_due_date->name;
1829 $logger->debug("circulator: building circulation ".
1830 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1832 $circ->duration($policy->{duration});
1833 $circ->recurring_fine($policy->{recurring_fine});
1834 $circ->duration_rule($duration->name);
1835 $circ->recurring_fine_rule($recurring->name);
1836 $circ->max_fine_rule($max->name);
1837 $circ->max_fine($policy->{max_fine});
1838 $circ->fine_interval($recurring->recurrence_interval);
1839 $circ->renewal_remaining($duration->max_renewals);
1840 $circ->grace_period($policy->{grace_period});
1844 $logger->info("circulator: copy found with an unlimited circ duration");
1845 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1846 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1847 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1848 $circ->renewal_remaining(0);
1849 $circ->grace_period(0);
1852 $circ->target_copy( $copy->id );
1853 $circ->usr( $patron->id );
1854 $circ->circ_lib( $self->circ_lib );
1855 $circ->workstation($self->editor->requestor->wsid)
1856 if defined $self->editor->requestor->wsid;
1858 # renewals maintain a link to the parent circulation
1859 $circ->parent_circ($self->parent_circ);
1861 if( $self->is_renewal ) {
1862 $circ->opac_renewal('t') if $self->opac_renewal;
1863 $circ->phone_renewal('t') if $self->phone_renewal;
1864 $circ->desk_renewal('t') if $self->desk_renewal;
1865 $circ->renewal_remaining($self->renewal_remaining);
1866 $circ->circ_staff($self->editor->requestor->id);
1870 # if the user provided an overiding checkout time,
1871 # (e.g. the checkout really happened several hours ago), then
1872 # we apply that here. Does this need a perm??
1873 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1874 if $self->checkout_time;
1876 # if a patron is renewing, 'requestor' will be the patron
1877 $circ->circ_staff($self->editor->requestor->id);
1878 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1883 sub do_reservation_pickup {
1886 $self->log_me("do_reservation_pickup()");
1888 $self->reservation->pickup_time('now');
1891 $self->reservation->current_resource &&
1892 $U->is_true($self->reservation->target_resource_type->catalog_item)
1894 # We used to try to set $self->copy and $self->patron here,
1895 # but that should already be done.
1897 $self->run_checkout_scripts(1);
1899 my $duration = $self->duration_rule;
1900 my $max = $self->max_fine_rule;
1901 my $recurring = $self->recurring_fines_rule;
1903 if ($duration && $max && $recurring) {
1904 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1906 my $dname = $duration->name;
1907 my $mname = $max->name;
1908 my $rname = $recurring->name;
1910 $logger->debug("circulator: updating reservation ".
1911 "with duration=$dname, maxfine=$mname, recurring=$rname");
1913 $self->reservation->fine_amount($policy->{recurring_fine});
1914 $self->reservation->max_fine($policy->{max_fine});
1915 $self->reservation->fine_interval($recurring->recurrence_interval);
1918 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1919 $self->update_copy();
1922 $self->reservation->fine_amount(
1923 $self->reservation->target_resource_type->fine_amount
1925 $self->reservation->max_fine(
1926 $self->reservation->target_resource_type->max_fine
1928 $self->reservation->fine_interval(
1929 $self->reservation->target_resource_type->fine_interval
1933 $self->update_reservation();
1936 sub do_reservation_return {
1938 my $request = shift;
1940 $self->log_me("do_reservation_return()");
1942 if (not ref $self->reservation) {
1943 my ($reservation, $evt) =
1944 $U->fetch_booking_reservation($self->reservation);
1945 return $self->bail_on_events($evt) if $evt;
1946 $self->reservation($reservation);
1949 $self->generate_fines(1);
1950 $self->reservation->return_time('now');
1951 $self->update_reservation();
1952 $self->reshelve_copy if $self->copy;
1954 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1955 $self->copy( $self->reservation->current_resource->catalog_item );
1959 sub booking_adjusted_due_date {
1961 my $circ = $self->circ;
1962 my $copy = $self->copy;
1964 return undef unless $self->use_booking;
1968 if( $self->due_date ) {
1970 return $self->bail_on_events($self->editor->event)
1971 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1973 $circ->due_date(cleanse_ISO8601($self->due_date));
1977 return unless $copy and $circ->due_date;
1980 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1981 if (@$booking_items) {
1982 my $booking_item = $booking_items->[0];
1983 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1985 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1986 my $shorten_circ_setting = $resource_type->elbow_room ||
1987 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1990 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1991 my $bookings = $booking_ses->request(
1992 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1993 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1995 $booking_ses->disconnect;
1997 my $dt_parser = DateTime::Format::ISO8601->new;
1998 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2000 for my $bid (@$bookings) {
2002 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2004 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2005 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2007 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2008 if ($booking_start < DateTime->now);
2011 if ($U->is_true($stop_circ_setting)) {
2012 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2014 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2015 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2018 # We set the circ duration here only to affect the logic that will
2019 # later (in a DB trigger) mangle the time part of the due date to
2020 # 11:59pm. Having any circ duration that is not a whole number of
2021 # days is enough to prevent the "correction."
2022 my $new_circ_duration = $due_date->epoch - time;
2023 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2024 $circ->duration("$new_circ_duration seconds");
2026 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2030 return $self->bail_on_events($self->editor->event)
2031 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2037 sub apply_modified_due_date {
2039 my $shift_earlier = shift;
2040 my $circ = $self->circ;
2041 my $copy = $self->copy;
2043 if( $self->due_date ) {
2045 return $self->bail_on_events($self->editor->event)
2046 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2048 $circ->due_date(cleanse_ISO8601($self->due_date));
2052 # if the due_date lands on a day when the location is closed
2053 return unless $copy and $circ->due_date;
2055 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2057 # due-date overlap should be determined by the location the item
2058 # is checked out from, not the owning or circ lib of the item
2059 my $org = $self->circ_lib;
2061 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2062 " with an item due date of ".$circ->due_date );
2064 my $dateinfo = $U->storagereq(
2065 'open-ils.storage.actor.org_unit.closed_date.overlap',
2066 $org, $circ->due_date );
2069 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2070 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2072 # XXX make the behavior more dynamic
2073 # for now, we just push the due date to after the close date
2074 if ($shift_earlier) {
2075 $circ->due_date($dateinfo->{start});
2077 $circ->due_date($dateinfo->{end});
2085 sub create_due_date {
2086 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2088 # if there is a raw time component (e.g. from postgres),
2089 # turn it into an interval that interval_to_seconds can parse
2090 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2092 # for now, use the server timezone. TODO: use workstation org timezone
2093 my $due_date = DateTime->now(time_zone => 'local');
2095 # add the circ duration
2096 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2099 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2100 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2101 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2106 # return ISO8601 time with timezone
2107 return $due_date->strftime('%FT%T%z');
2112 sub make_precat_copy {
2114 my $copy = $self->copy;
2117 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2119 $copy->editor($self->editor->requestor->id);
2120 $copy->edit_date('now');
2121 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2122 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2123 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2124 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2125 $self->update_copy();
2129 $logger->info("circulator: Creating a new precataloged ".
2130 "copy in checkout with barcode " . $self->copy_barcode);
2132 $copy = Fieldmapper::asset::copy->new;
2133 $copy->circ_lib($self->circ_lib);
2134 $copy->creator($self->editor->requestor->id);
2135 $copy->editor($self->editor->requestor->id);
2136 $copy->barcode($self->copy_barcode);
2137 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2138 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2139 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2141 $copy->dummy_title($self->dummy_title || "");
2142 $copy->dummy_author($self->dummy_author || "");
2143 $copy->dummy_isbn($self->dummy_isbn || "");
2144 $copy->circ_modifier($self->circ_modifier);
2147 # See if we need to override the circ_lib for the copy with a configured circ_lib
2148 # Setting is shortname of the org unit
2149 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2150 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2152 if($precat_circ_lib) {
2153 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2156 $self->bail_on_events($self->editor->event);
2160 $copy->circ_lib($org->id);
2164 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2166 $self->push_events($self->editor->event);
2170 # this is a little bit of a hack, but we need to
2171 # get the copy into the script runner
2172 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2176 sub checkout_noncat {
2182 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2183 my $count = $self->noncat_count || 1;
2184 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2186 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2190 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2191 $self->editor->requestor->id,
2199 $self->push_events($evt);
2210 $self->log_me("do_checkin()");
2212 return $self->bail_on_events(
2213 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2216 # the renew code and mk_env should have already found our circulation object
2217 unless( $self->circ ) {
2219 my $circs = $self->editor->search_action_circulation(
2220 { target_copy => $self->copy->id, checkin_time => undef });
2222 $self->circ($$circs[0]);
2224 # for now, just warn if there are multiple open circs on a copy
2225 $logger->warn("circulator: we have ".scalar(@$circs).
2226 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2229 # run the fine generator against this circ, if this circ is there
2230 $self->generate_fines_start if $self->circ;
2232 if( $self->checkin_check_holds_shelf() ) {
2233 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2234 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2235 $self->checkin_flesh_events;
2239 unless( $self->is_renewal ) {
2240 return $self->bail_on_events($self->editor->event)
2241 unless $self->editor->allowed('COPY_CHECKIN');
2244 $self->push_events($self->check_copy_alert());
2245 $self->push_events($self->check_checkin_copy_status());
2247 # if the circ is marked as 'claims returned', add the event to the list
2248 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2249 if ($self->circ and $self->circ->stop_fines
2250 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2252 $self->check_circ_deposit();
2254 # handle the overridable events
2255 $self->override_events unless $self->is_renewal;
2256 return if $self->bail_out;
2260 $self->editor->search_action_transit_copy(
2261 { target_copy => $self->copy->id, dest_recv_time => undef }
2267 $self->checkin_handle_circ;
2268 return if $self->bail_out;
2269 $self->checkin_changed(1);
2271 } elsif( $self->transit ) {
2272 my $hold_transit = $self->process_received_transit;
2273 $self->checkin_changed(1);
2275 if( $self->bail_out ) {
2276 $self->checkin_flesh_events;
2280 if( my $e = $self->check_checkin_copy_status() ) {
2281 # If the original copy status is special, alert the caller
2282 my $ev = $self->events;
2283 $self->events([$e]);
2284 $self->override_events;
2285 return if $self->bail_out;
2289 if( $hold_transit or
2290 $U->copy_status($self->copy->status)->id
2291 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2294 if( $hold_transit ) {
2295 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2297 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2302 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2304 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2305 $self->reshelve_copy(1);
2306 $self->cancelled_hold_transit(1);
2307 $self->notify_hold(0); # don't notify for cancelled holds
2308 return if $self->bail_out;
2312 # hold transited to correct location
2313 $self->checkin_flesh_events;
2318 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2320 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2321 " that is in-transit, but there is no transit.. repairing");
2322 $self->reshelve_copy(1);
2323 return if $self->bail_out;
2326 if( $self->is_renewal ) {
2327 $self->finish_fines_and_voiding;
2328 return if $self->bail_out;
2329 $self->push_events(OpenILS::Event->new('SUCCESS'));
2333 # ------------------------------------------------------------------------------
2334 # Circulations and transits are now closed where necessary. Now go on to see if
2335 # this copy can fulfill a hold or needs to be routed to a different location
2336 # ------------------------------------------------------------------------------
2338 my $needed_for_something = 0; # formerly "needed_for_hold"
2340 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2342 if (!$self->remote_hold) {
2343 if ($self->use_booking) {
2344 my $potential_hold = $self->hold_capture_is_possible;
2345 my $potential_reservation = $self->reservation_capture_is_possible;
2347 if ($potential_hold and $potential_reservation) {
2348 $logger->info("circulator: item could fulfill either hold or reservation");
2349 $self->push_events(new OpenILS::Event(
2350 "HOLD_RESERVATION_CONFLICT",
2351 "hold" => $potential_hold,
2352 "reservation" => $potential_reservation
2354 return if $self->bail_out;
2355 } elsif ($potential_hold) {
2356 $needed_for_something =
2357 $self->attempt_checkin_hold_capture;
2358 } elsif ($potential_reservation) {
2359 $needed_for_something =
2360 $self->attempt_checkin_reservation_capture;
2363 $needed_for_something = $self->attempt_checkin_hold_capture;
2366 return if $self->bail_out;
2368 unless($needed_for_something) {
2369 my $circ_lib = (ref $self->copy->circ_lib) ?
2370 $self->copy->circ_lib->id : $self->copy->circ_lib;
2372 if( $self->remote_hold ) {
2373 $circ_lib = $self->remote_hold->pickup_lib;
2374 $logger->warn("circulator: Copy ".$self->copy->barcode.
2375 " is on a remote hold's shelf, sending to $circ_lib");
2378 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2380 if( $circ_lib == $self->circ_lib) {
2381 # copy is where it needs to be, either for hold or reshelving
2383 $self->checkin_handle_precat();
2384 return if $self->bail_out;
2387 # copy needs to transit "home", or stick here if it's a floating copy
2389 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2390 $self->checkin_changed(1);
2391 $self->copy->circ_lib( $self->circ_lib );
2394 my $bc = $self->copy->barcode;
2395 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2396 $self->checkin_build_copy_transit($circ_lib);
2397 return if $self->bail_out;
2398 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2402 } else { # no-op checkin
2403 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2404 $self->checkin_changed(1);
2405 $self->copy->circ_lib( $self->circ_lib );
2410 if($self->claims_never_checked_out and
2411 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2413 # the item was not supposed to be checked out to the user and should now be marked as missing
2414 $self->copy->status(OILS_COPY_STATUS_MISSING);
2418 $self->reshelve_copy unless $needed_for_something;
2421 return if $self->bail_out;
2423 unless($self->checkin_changed) {
2425 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2426 my $stat = $U->copy_status($self->copy->status)->id;
2428 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2429 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2430 $self->bail_out(1); # no need to commit anything
2434 $self->push_events(OpenILS::Event->new('SUCCESS'))
2435 unless @{$self->events};
2438 $self->finish_fines_and_voiding;
2440 OpenILS::Utils::Penalty->calculate_penalties(
2441 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2443 $self->checkin_flesh_events;
2447 sub finish_fines_and_voiding {
2449 return unless $self->circ;
2451 # gather any updates to the circ after fine generation, if there was a circ
2452 $self->generate_fines_finish;
2454 return unless $self->backdate or $self->void_overdues;
2456 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2457 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2459 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2460 $self->editor, $self->circ, $self->backdate, $note);
2462 return $self->bail_on_events($evt) if $evt;
2464 # make sure the circ isn't closed if we just voided some fines
2465 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2466 return $self->bail_on_events($evt) if $evt;
2472 # if a deposit was payed for this item, push the event
2473 sub check_circ_deposit {
2475 return unless $self->circ;
2476 my $deposit = $self->editor->search_money_billing(
2478 xact => $self->circ->id,
2480 }, {idlist => 1})->[0];
2482 $self->push_events(OpenILS::Event->new(
2483 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2488 my $force = $self->force || shift;
2489 my $copy = $self->copy;
2491 my $stat = $U->copy_status($copy->status)->id;
2494 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2495 $stat != OILS_COPY_STATUS_CATALOGING and
2496 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2497 $stat != OILS_COPY_STATUS_RESHELVING )) {
2499 $copy->status( OILS_COPY_STATUS_RESHELVING );
2501 $self->checkin_changed(1);
2506 # Returns true if the item is at the current location
2507 # because it was transited there for a hold and the
2508 # hold has not been fulfilled
2509 sub checkin_check_holds_shelf {
2511 return 0 unless $self->copy;
2514 $U->copy_status($self->copy->status)->id ==
2515 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2517 # find the hold that put us on the holds shelf
2518 my $holds = $self->editor->search_action_hold_request(
2520 current_copy => $self->copy->id,
2521 capture_time => { '!=' => undef },
2522 fulfillment_time => undef,
2523 cancel_time => undef,
2528 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2529 $self->reshelve_copy(1);
2533 my $hold = $$holds[0];
2535 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2536 $hold->id. "] for copy ".$self->copy->barcode);
2538 if( $hold->pickup_lib == $self->circ_lib ) {
2539 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2543 $logger->info("circulator: hold is not for here..");
2544 $self->remote_hold($hold);
2549 sub checkin_handle_precat {
2551 my $copy = $self->copy;
2553 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2554 $copy->status(OILS_COPY_STATUS_CATALOGING);
2555 $self->update_copy();
2556 $self->checkin_changed(1);
2557 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2562 sub checkin_build_copy_transit {
2565 my $copy = $self->copy;
2566 my $transit = Fieldmapper::action::transit_copy->new;
2568 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2569 $logger->info("circulator: transiting copy to $dest");
2571 $transit->source($self->circ_lib);
2572 $transit->dest($dest);
2573 $transit->target_copy($copy->id);
2574 $transit->source_send_time('now');
2575 $transit->copy_status( $U->copy_status($copy->status)->id );
2577 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2579 return $self->bail_on_events($self->editor->event)
2580 unless $self->editor->create_action_transit_copy($transit);
2582 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2584 $self->checkin_changed(1);
2588 sub hold_capture_is_possible {
2590 my $copy = $self->copy;
2592 # we've been explicitly told not to capture any holds
2593 return 0 if $self->capture eq 'nocapture';
2595 # See if this copy can fulfill any holds
2596 my $hold = $holdcode->find_nearest_permitted_hold(
2597 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2599 return undef if ref $hold eq "HASH" and
2600 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2604 sub reservation_capture_is_possible {
2606 my $copy = $self->copy;
2608 # we've been explicitly told not to capture any holds
2609 return 0 if $self->capture eq 'nocapture';
2611 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2612 my $resv = $booking_ses->request(
2613 "open-ils.booking.reservations.could_capture",
2614 $self->editor->authtoken, $copy->barcode
2616 $booking_ses->disconnect;
2617 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2618 $self->push_events($resv);
2624 # returns true if the item was used (or may potentially be used
2625 # in subsequent calls) to capture a hold.
2626 sub attempt_checkin_hold_capture {
2628 my $copy = $self->copy;
2630 # we've been explicitly told not to capture any holds
2631 return 0 if $self->capture eq 'nocapture';
2633 # See if this copy can fulfill any holds
2634 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2635 $self->editor, $copy, $self->editor->requestor );
2638 $logger->debug("circulator: no potential permitted".
2639 "holds found for copy ".$copy->barcode);
2643 if($self->capture ne 'capture') {
2644 # see if this item is in a hold-capture-delay location
2645 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2646 if($U->is_true($location->hold_verify)) {
2647 $self->bail_on_events(
2648 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2653 $self->retarget($retarget);
2655 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2657 $hold->current_copy($copy->id);
2658 $hold->capture_time('now');
2659 $self->put_hold_on_shelf($hold)
2660 if $hold->pickup_lib == $self->circ_lib;
2662 # prevent DB errors caused by fetching
2663 # holds from storage, and updating through cstore
2664 $hold->clear_fulfillment_time;
2665 $hold->clear_fulfillment_staff;
2666 $hold->clear_fulfillment_lib;
2667 $hold->clear_expire_time;
2668 $hold->clear_cancel_time;
2669 $hold->clear_prev_check_time unless $hold->prev_check_time;
2671 $self->bail_on_events($self->editor->event)
2672 unless $self->editor->update_action_hold_request($hold);
2674 $self->checkin_changed(1);
2676 return 0 if $self->bail_out;
2678 if( $hold->pickup_lib == $self->circ_lib ) {
2680 # This hold was captured in the correct location
2681 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2682 $self->push_events(OpenILS::Event->new('SUCCESS'));
2684 #$self->do_hold_notify($hold->id);
2685 $self->notify_hold($hold->id);
2689 # Hold needs to be picked up elsewhere. Build a hold
2690 # transit and route the item.
2691 $self->checkin_build_hold_transit();
2692 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2693 return 0 if $self->bail_out;
2694 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2697 # make sure we save the copy status
2702 sub attempt_checkin_reservation_capture {
2704 my $copy = $self->copy;
2706 # we've been explicitly told not to capture any holds
2707 return 0 if $self->capture eq 'nocapture';
2709 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2710 my $evt = $booking_ses->request(
2711 "open-ils.booking.resources.capture_for_reservation",
2712 $self->editor->authtoken,
2714 1 # don't update copy - we probably have it locked
2716 $booking_ses->disconnect;
2718 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2720 "open-ils.booking.resources.capture_for_reservation " .
2721 "didn't return an event!"
2725 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2726 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2728 # not-transferable is an error event we'll pass on the user
2729 $logger->warn("reservation capture attempted against non-transferable item");
2730 $self->push_events($evt);
2732 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2733 # Re-retrieve copy as reservation capture may have changed
2734 # its status and whatnot.
2736 "circulator: booking capture win on copy " . $self->copy->id
2738 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2740 "circulator: changing copy " . $self->copy->id .
2741 "'s status from " . $self->copy->status . " to " .
2744 $self->copy->status($new_copy_status);
2747 $self->reservation($evt->{"payload"}->{"reservation"});
2749 if (exists $evt->{"payload"}->{"transit"}) {
2753 "org" => $evt->{"payload"}->{"transit"}->dest
2757 $self->checkin_changed(1);
2761 # other results are treated as "nothing to capture"
2765 sub do_hold_notify {
2766 my( $self, $holdid ) = @_;
2768 my $e = new_editor(xact => 1);
2769 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2771 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2772 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2774 $logger->info("circulator: running delayed hold notify process");
2776 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2777 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2779 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2780 hold_id => $holdid, requestor => $self->editor->requestor);
2782 $logger->debug("circulator: built hold notifier");
2784 if(!$notifier->event) {
2786 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2788 my $stat = $notifier->send_email_notify;
2789 if( $stat == '1' ) {
2790 $logger->info("circulator: hold notify succeeded for hold $holdid");
2794 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2797 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2801 sub retarget_holds {
2803 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2804 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2805 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2806 # no reason to wait for the return value
2810 sub checkin_build_hold_transit {
2813 my $copy = $self->copy;
2814 my $hold = $self->hold;
2815 my $trans = Fieldmapper::action::hold_transit_copy->new;
2817 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2819 $trans->hold($hold->id);
2820 $trans->source($self->circ_lib);
2821 $trans->dest($hold->pickup_lib);
2822 $trans->source_send_time("now");
2823 $trans->target_copy($copy->id);
2825 # when the copy gets to its destination, it will recover
2826 # this status - put it onto the holds shelf
2827 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2829 return $self->bail_on_events($self->editor->event)
2830 unless $self->editor->create_action_hold_transit_copy($trans);
2835 sub process_received_transit {
2837 my $copy = $self->copy;
2838 my $copyid = $self->copy->id;
2840 my $status_name = $U->copy_status($copy->status)->name;
2841 $logger->debug("circulator: attempting transit receive on ".
2842 "copy $copyid. Copy status is $status_name");
2844 my $transit = $self->transit;
2846 if( $transit->dest != $self->circ_lib ) {
2847 # - this item is in-transit to a different location
2849 my $tid = $transit->id;
2850 my $loc = $self->circ_lib;
2851 my $dest = $transit->dest;
2853 $logger->info("circulator: Fowarding transit on copy which is destined ".
2854 "for a different location. transit=$tid, copy=$copyid, current ".
2855 "location=$loc, destination location=$dest");
2857 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2859 # grab the associated hold object if available
2860 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2861 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2863 return $self->bail_on_events($evt);
2866 # The transit is received, set the receive time
2867 $transit->dest_recv_time('now');
2868 $self->bail_on_events($self->editor->event)
2869 unless $self->editor->update_action_transit_copy($transit);
2871 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2873 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2874 $copy->status( $transit->copy_status );
2875 $self->update_copy();
2876 return if $self->bail_out;
2880 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2882 # hold has arrived at destination, set shelf time
2883 $self->put_hold_on_shelf($hold);
2884 $self->bail_on_events($self->editor->event)
2885 unless $self->editor->update_action_hold_request($hold);
2886 return if $self->bail_out;
2888 $self->notify_hold($hold_transit->hold);
2893 OpenILS::Event->new(
2896 payload => { transit => $transit, holdtransit => $hold_transit } ));
2898 return $hold_transit;
2902 # ------------------------------------------------------------------
2903 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2904 # ------------------------------------------------------------------
2905 sub put_hold_on_shelf {
2906 my($self, $hold) = @_;
2908 $hold->shelf_time('now');
2910 my $shelf_expire = $U->ou_ancestor_setting_value(
2911 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2914 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2915 my $expire_time = DateTime->now->add(seconds => $seconds);
2916 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2924 sub generate_fines {
2926 my $reservation = shift;
2928 $self->generate_fines_start($reservation);
2929 $self->generate_fines_finish($reservation);
2934 sub generate_fines_start {
2936 my $reservation = shift;
2937 my $dt_parser = DateTime::Format::ISO8601->new;
2939 my $obj = $reservation ? $self->reservation : $self->circ;
2941 # If we have a grace period
2942 if($obj->can('grace_period')) {
2943 # Parse out the due date
2944 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
2945 # Add the grace period to the due date
2946 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
2947 # Don't generate fines on circs still in grace period
2948 return undef if ($due_date > DateTime->now);
2951 if (!exists($self->{_gen_fines_req})) {
2952 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
2954 'open-ils.storage.action.circulation.overdue.generate_fines',
2962 sub generate_fines_finish {
2964 my $reservation = shift;
2966 return undef unless $self->{_gen_fines_req};
2968 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2970 $self->{_gen_fines_req}->wait_complete;
2971 delete($self->{_gen_fines_req});
2973 # refresh the circ in case the fine generator set the stop_fines field
2974 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2975 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2980 sub checkin_handle_circ {
2982 my $circ = $self->circ;
2983 my $copy = $self->copy;
2987 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2989 # backdate the circ if necessary
2990 if($self->backdate) {
2991 my $evt = $self->checkin_handle_backdate;
2992 return $self->bail_on_events($evt) if $evt;
2995 if(!$circ->stop_fines) {
2996 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2997 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2998 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2999 $circ->stop_fines_time('now');
3000 $circ->stop_fines_time($self->backdate) if $self->backdate;
3003 # Set the checkin vars since we have the item
3004 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3006 # capture the true scan time for back-dated checkins
3007 $circ->checkin_scan_time('now');
3009 $circ->checkin_staff($self->editor->requestor->id);
3010 $circ->checkin_lib($self->circ_lib);
3011 $circ->checkin_workstation($self->editor->requestor->wsid);
3013 my $circ_lib = (ref $self->copy->circ_lib) ?
3014 $self->copy->circ_lib->id : $self->copy->circ_lib;
3015 my $stat = $U->copy_status($self->copy->status)->id;
3017 # immediately available keeps items lost or missing items from going home before being handled
3018 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3019 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3022 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3024 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3025 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3027 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3031 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3033 $self->checkin_handle_lost($circ_lib);
3037 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3042 # see if there are any fines owed on this circ. if not, close it
3043 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3044 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3046 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3048 return $self->bail_on_events($self->editor->event)
3049 unless $self->editor->update_action_circulation($circ);
3055 # ------------------------------------------------------------------
3056 # See if we need to void billings for lost checkin
3057 # ------------------------------------------------------------------
3058 sub checkin_handle_lost {
3060 my $circ_lib = shift;
3061 my $circ = $self->circ;
3063 my $max_return = $U->ou_ancestor_setting_value(
3064 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3069 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3070 $tm[5] -= 1 if $tm[5] > 0;
3071 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3073 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3074 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3076 $max_return = 0 if $today < $last_chance;
3079 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3081 my $void_lost = $U->ou_ancestor_setting_value(
3082 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3083 my $void_lost_fee = $U->ou_ancestor_setting_value(
3084 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3085 my $restore_od = $U->ou_ancestor_setting_value(
3086 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3088 $self->checkin_handle_lost_now_found(3) if $void_lost;
3089 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3090 $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3093 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3098 sub checkin_handle_backdate {
3101 # ------------------------------------------------------------------
3102 # clean up the backdate for date comparison
3103 # XXX We are currently taking the due-time from the original due-date,
3104 # not the input. Do we need to do this? This certainly interferes with
3105 # backdating of hourly checkouts, but that is likely a very rare case.
3106 # ------------------------------------------------------------------
3107 my $bd = cleanse_ISO8601($self->backdate);
3108 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3109 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3110 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3112 $self->backdate($bd);
3117 sub check_checkin_copy_status {
3119 my $copy = $self->copy;
3121 my $status = $U->copy_status($copy->status)->id;
3124 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3125 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3126 $status == OILS_COPY_STATUS_IN_PROCESS ||
3127 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3128 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3129 $status == OILS_COPY_STATUS_CATALOGING ||
3130 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3131 $status == OILS_COPY_STATUS_RESHELVING );
3133 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3134 if( $status == OILS_COPY_STATUS_LOST );
3136 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3137 if( $status == OILS_COPY_STATUS_MISSING );
3139 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3144 # --------------------------------------------------------------------------
3145 # On checkin, we need to return as many relevant objects as we can
3146 # --------------------------------------------------------------------------
3147 sub checkin_flesh_events {
3150 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3151 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3152 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3155 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3158 if($self->hold and !$self->hold->cancel_time) {
3159 $hold = $self->hold;
3160 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3164 # if we checked in a circulation, flesh the billing summary data
3165 $self->circ->billable_transaction(
3166 $self->editor->retrieve_money_billable_transaction([
3168 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3174 # flesh some patron fields before returning
3176 $self->editor->retrieve_actor_user([
3181 au => ['card', 'billing_address', 'mailing_address']
3188 for my $evt (@{$self->events}) {
3191 $payload->{copy} = $U->unflesh_copy($self->copy);
3192 $payload->{record} = $record,
3193 $payload->{circ} = $self->circ;
3194 $payload->{transit} = $self->transit;
3195 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3196 $payload->{hold} = $hold;
3197 $payload->{patron} = $self->patron;
3198 $payload->{reservation} = $self->reservation
3199 unless (not $self->reservation or $self->reservation->cancel_time);
3201 $evt->{payload} = $payload;
3206 my( $self, $msg ) = @_;
3207 my $bc = ($self->copy) ? $self->copy->barcode :
3210 my $usr = ($self->patron) ? $self->patron->id : "";
3211 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3212 ", recipient=$usr, copy=$bc");
3218 $self->log_me("do_renew()");
3220 # Make sure there is an open circ to renew that is not
3221 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3222 my $usrid = $self->patron->id if $self->patron;
3223 my $circ = $self->editor->search_action_circulation({
3224 target_copy => $self->copy->id,
3225 xact_finish => undef,
3226 ($usrid ? (usr => $usrid) : ()),
3228 {stop_fines => undef},
3229 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3233 return $self->bail_on_events($self->editor->event) unless $circ;
3235 # A user is not allowed to renew another user's items without permission
3236 unless( $circ->usr eq $self->editor->requestor->id ) {
3237 return $self->bail_on_events($self->editor->events)
3238 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3241 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3242 if $circ->renewal_remaining < 1;
3244 # -----------------------------------------------------------------
3246 $self->parent_circ($circ->id);
3247 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3250 # Run the fine generator against the old circ
3251 $self->generate_fines_start;
3253 $self->run_renew_permit;
3256 $self->do_checkin();
3257 return if $self->bail_out;
3259 unless( $self->permit_override ) {
3261 return if $self->bail_out;
3262 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3263 $self->remove_event('ITEM_NOT_CATALOGED');
3266 $self->override_events;
3267 return if $self->bail_out;
3270 $self->do_checkout();
3275 my( $self, $evt ) = @_;
3276 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3277 $logger->debug("circulator: removing event from list: $evt");
3278 my @events = @{$self->events};
3279 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3284 my( $self, $evt ) = @_;
3285 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3286 return grep { $_->{textcode} eq $evt } @{$self->events};
3291 sub run_renew_permit {
3294 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3295 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3296 $self->editor, $self->copy, $self->editor->requestor, 1
3298 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3301 if(!$self->legacy_script_support) {
3302 my $results = $self->run_indb_circ_test;
3303 $self->push_events($self->matrix_test_result_events)
3304 unless $self->circ_test_success;
3307 my $runner = $self->script_runner;
3309 $runner->load($self->circ_permit_renew);
3310 my $result = $runner->run or
3311 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3312 if ($result->{"events"}) {
3314 map { new OpenILS::Event($_) } @{$result->{"events"}}
3317 "circulator: circ_permit_renew for user " .
3318 $self->patron->id . " returned " .
3319 scalar(@{$result->{"events"}}) . " event(s)"
3323 $self->mk_script_runner;
3326 $logger->debug("circulator: re-creating script runner to be safe");
3330 # XXX: The primary mechanism for storing circ history is now handled
3331 # by tracking real circulation objects instead of bibs in a bucket.
3332 # However, this code is disabled by default and could be useful
3333 # some day, so may as well leave it for now.
3334 sub append_reading_list {
3338 $self->is_checkout and
3344 # verify history is globally enabled and uses the bucket mechanism
3345 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3346 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3348 return undef unless $htype and $htype eq 'bucket';
3350 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3352 # verify the patron wants to retain the hisory
3353 my $setting = $e->search_actor_user_setting(
3354 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3356 unless($setting and $setting->value) {
3361 my $bkt = $e->search_container_copy_bucket(
3362 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3367 # find the next item position
3368 my $last_item = $e->search_container_copy_bucket_item(
3369 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3370 $pos = $last_item->pos + 1 if $last_item;
3373 # create the history bucket if necessary
3374 $bkt = Fieldmapper::container::copy_bucket->new;
3375 $bkt->owner($self->patron->id);
3377 $bkt->btype('circ_history');
3379 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3382 my $item = Fieldmapper::container::copy_bucket_item->new;
3384 $item->bucket($bkt->id);
3385 $item->target_copy($self->copy->id);
3388 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3395 sub make_trigger_events {
3397 return unless $self->circ;
3398 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3399 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3400 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3405 sub checkin_handle_lost_now_found {
3406 my ($self, $bill_type) = @_;
3408 # ------------------------------------------------------------------
3409 # remove charge from patron's account if lost item is returned
3410 # ------------------------------------------------------------------
3412 my $bills = $self->editor->search_money_billing(
3414 xact => $self->circ->id,
3419 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3420 for my $bill (@$bills) {
3421 if( !$U->is_true($bill->voided) ) {
3422 $logger->info("lost item returned - voiding bill ".$bill->id);
3424 $bill->void_time('now');
3425 $bill->voider($self->editor->requestor->id);
3426 my $note = ($bill->note) ? $bill->note . "\n" : '';
3427 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3429 $self->bail_on_events($self->editor->event)
3430 unless $self->editor->update_money_billing($bill);
3435 sub checkin_handle_lost_now_found_restore_od {
3438 # ------------------------------------------------------------------
3439 # restore those overdue charges voided when item was set to lost
3440 # ------------------------------------------------------------------
3442 my $ods = $self->editor->search_money_billing(
3444 xact => $self->circ->id,
3449 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3450 for my $bill (@$ods) {
3451 if( $U->is_true($bill->voided) ) {
3452 $logger->info("lost item returned - restoring overdue ".$bill->id);
3454 $bill->clear_void_time;
3455 $bill->voider($self->editor->requestor->id);
3456 my $note = ($bill->note) ? $bill->note . "\n" : '';
3457 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3459 $self->bail_on_events($self->editor->event)
3460 unless $self->editor->update_money_billing($bill);