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.checkout.inspect",
184 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
189 my( $self, $conn, $auth, $args ) = @_;
190 translate_legacy_args($args);
191 my $api = $self->api_name;
194 OpenILS::Application::Circ::Circulator->new($auth, %$args);
196 return circ_events($circulator) if $circulator->bail_out;
198 $circulator->use_booking(determine_booking_status());
200 # --------------------------------------------------------------------------
201 # First, check for a booking transit, as the barcode may not be a copy
202 # barcode, but a resource barcode, and nothing else in here will work
203 # --------------------------------------------------------------------------
205 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
206 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
207 if (@$resources) { # yes!
209 my $res_id_list = [ map { $_->id } @$resources ];
210 my $transit = $circulator->editor->search_action_reservation_transit_copy(
212 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
213 { order_by => { artc => 'source_send_time' }, limit => 1 }
215 )->[0]; # Any transit for this barcode?
217 if ($transit) { # yes! unwrap it.
219 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
220 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
222 my $success_event = new OpenILS::Event(
223 "SUCCESS", "payload" => {"reservation" => $reservation}
225 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
226 if (my $copy = $circulator->editor->search_asset_copy([
227 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
228 ])->[0]) { # got a copy
229 $copy->status( $transit->copy_status );
230 $copy->editor($circulator->editor->requestor->id);
231 $copy->edit_date('now');
232 $circulator->editor->update_asset_copy($copy);
233 $success_event->{"payload"}->{"record"} =
234 $U->record_to_mvr($copy->call_number->record);
235 $copy->call_number($copy->call_number->id);
236 $success_event->{"payload"}->{"copy"} = $copy;
240 $transit->dest_recv_time('now');
241 $circulator->editor->update_action_reservation_transit_copy( $transit );
243 $circulator->editor->commit;
244 # Formerly this branch just stopped here. Argh!
245 $conn->respond_complete($success_event);
253 # --------------------------------------------------------------------------
254 # Go ahead and load the script runner to make sure we have all
255 # of the objects we need
256 # --------------------------------------------------------------------------
258 if ($circulator->use_booking) {
259 $circulator->is_res_checkin($circulator->is_checkin(1))
260 if $api =~ /reservation.return/ or (
261 $api =~ /checkin/ and $circulator->seems_like_reservation()
264 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
267 $circulator->is_renewal(1) if $api =~ /renew/;
268 $circulator->is_checkin(1) if $api =~ /checkin/;
270 $circulator->mk_env();
271 $circulator->noop(1) if $circulator->claims_never_checked_out;
273 if($legacy_script_support and not $circulator->is_checkin) {
274 $circulator->mk_script_runner();
275 $circulator->legacy_script_support(1);
276 $circulator->circ_permit_patron($scripts{circ_permit_patron});
277 $circulator->circ_permit_copy($scripts{circ_permit_copy});
278 $circulator->circ_duration($scripts{circ_duration});
279 $circulator->circ_permit_renew($scripts{circ_permit_renew});
281 return circ_events($circulator) if $circulator->bail_out;
284 $circulator->override(1) if $api =~ /override/o;
286 if( $api =~ /checkout\.permit/ ) {
287 $circulator->do_permit();
289 } elsif( $api =~ /checkout.full/ ) {
291 # requesting a precat checkout implies that any required
292 # overrides have been performed. Go ahead and re-override.
293 $circulator->skip_permit_key(1);
294 $circulator->override(1) if $circulator->request_precat;
295 $circulator->do_permit();
296 $circulator->is_checkout(1);
297 unless( $circulator->bail_out ) {
298 $circulator->events([]);
299 $circulator->do_checkout();
302 } elsif( $circulator->is_res_checkout ) {
303 $circulator->do_reservation_pickup();
305 } elsif( $api =~ /inspect/ ) {
306 my $data = $circulator->do_inspect();
307 $circulator->editor->rollback;
310 } elsif( $api =~ /checkout/ ) {
311 $circulator->is_checkout(1);
312 $circulator->do_checkout();
314 } elsif( $circulator->is_res_checkin ) {
315 $circulator->do_reservation_return();
316 $circulator->do_checkin() if ($circulator->copy());
317 } elsif( $api =~ /checkin/ ) {
318 $circulator->do_checkin();
320 } elsif( $api =~ /renew/ ) {
321 $circulator->is_renewal(1);
322 $circulator->do_renew();
325 if( $circulator->bail_out ) {
328 # make sure no success event accidentally slip in
330 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
333 my @e = @{$circulator->events};
334 push( @ee, $_->{textcode} ) for @e;
335 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
337 $circulator->editor->rollback;
340 $circulator->editor->commit;
343 $circulator->script_runner->cleanup if $circulator->script_runner;
345 $conn->respond_complete(circ_events($circulator));
347 unless($circulator->bail_out) {
348 $circulator->do_hold_notify($circulator->notify_hold)
349 if $circulator->notify_hold;
350 $circulator->retarget_holds if $circulator->retarget;
351 $circulator->append_reading_list;
352 $circulator->make_trigger_events;
358 my @e = @{$circ->events};
359 # if we have multiple events, SUCCESS should not be one of them;
360 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
361 return (@e == 1) ? $e[0] : \@e;
365 sub translate_legacy_args {
368 if( $$args{barcode} ) {
369 $$args{copy_barcode} = $$args{barcode};
370 delete $$args{barcode};
373 if( $$args{copyid} ) {
374 $$args{copy_id} = $$args{copyid};
375 delete $$args{copyid};
378 if( $$args{patronid} ) {
379 $$args{patron_id} = $$args{patronid};
380 delete $$args{patronid};
383 if( $$args{patron} and !ref($$args{patron}) ) {
384 $$args{patron_id} = $$args{patron};
385 delete $$args{patron};
389 if( $$args{noncat} ) {
390 $$args{is_noncat} = $$args{noncat};
391 delete $$args{noncat};
394 if( $$args{precat} ) {
395 $$args{is_precat} = $$args{request_precat} = $$args{precat};
396 delete $$args{precat};
402 # --------------------------------------------------------------------------
403 # This package actually manages all of the circulation logic
404 # --------------------------------------------------------------------------
405 package OpenILS::Application::Circ::Circulator;
406 use strict; use warnings;
407 use vars q/$AUTOLOAD/;
409 use OpenILS::Utils::Fieldmapper;
410 use OpenSRF::Utils::Cache;
411 use Digest::MD5 qw(md5_hex);
412 use DateTime::Format::ISO8601;
413 use OpenILS::Utils::PermitHold;
414 use OpenSRF::Utils qw/:datetime/;
415 use OpenSRF::Utils::SettingsClient;
416 use OpenILS::Application::Circ::Holds;
417 use OpenILS::Application::Circ::Transit;
418 use OpenSRF::Utils::Logger qw(:logger);
419 use OpenILS::Utils::CStoreEditor qw/:funcs/;
420 use OpenILS::Application::Circ::ScriptBuilder;
421 use OpenILS::Const qw/:const/;
422 use OpenILS::Utils::Penalty;
423 use OpenILS::Application::Circ::CircCommon;
426 my $holdcode = "OpenILS::Application::Circ::Holds";
427 my $transcode = "OpenILS::Application::Circ::Transit";
433 # --------------------------------------------------------------------------
434 # Add a pile of automagic getter/setter methods
435 # --------------------------------------------------------------------------
436 my @AUTOLOAD_FIELDS = qw/
483 recurring_fines_level
496 cancelled_hold_transit
503 circ_matrix_matchpoint
505 legacy_script_support
515 claims_never_checked_out
525 my $type = ref($self) or die "$self is not an object";
527 my $name = $AUTOLOAD;
530 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
531 $logger->error("circulator: $type: invalid autoload field: $name");
532 die "$type: invalid autoload field: $name\n"
537 *{"${type}::${name}"} = sub {
540 $s->{$name} = $v if defined $v;
544 return $self->$name($data);
549 my( $class, $auth, %args ) = @_;
550 $class = ref($class) || $class;
551 my $self = bless( {}, $class );
554 $self->editor(new_editor(xact => 1, authtoken => $auth));
556 unless( $self->editor->checkauth ) {
557 $self->bail_on_events($self->editor->event);
561 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
563 $self->$_($args{$_}) for keys %args;
566 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
568 # if this is a renewal, default to desk_renewal
569 $self->desk_renewal(1) unless
570 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
572 $self->capture('') unless $self->capture;
574 unless(%user_groups) {
575 my $gps = $self->editor->retrieve_all_permission_grp_tree;
576 %user_groups = map { $_->id => $_ } @$gps;
583 # --------------------------------------------------------------------------
584 # True if we should discontinue processing
585 # --------------------------------------------------------------------------
587 my( $self, $bool ) = @_;
588 if( defined $bool ) {
589 $logger->info("circulator: BAILING OUT") if $bool;
590 $self->{bail_out} = $bool;
592 return $self->{bail_out};
597 my( $self, @evts ) = @_;
600 $e->{payload} = $self->copy if
601 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
603 $logger->info("circulator: pushing event ".$e->{textcode});
604 push( @{$self->events}, $e ) unless
605 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
611 return '' if $self->skip_permit_key;
612 my $key = md5_hex( time() . rand() . "$$" );
613 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
614 return $self->permit_key($key);
617 sub check_permit_key {
619 return 1 if $self->skip_permit_key;
620 my $key = $self->permit_key;
621 return 0 unless $key;
622 my $k = "oils_permit_key_$key";
623 my $one = $self->cache_handle->get_cache($k);
624 $self->cache_handle->delete_cache($k);
625 return ($one) ? 1 : 0;
628 sub seems_like_reservation {
631 # Some words about the following method:
632 # 1) It requires the VIEW_USER permission, but that's not an
633 # issue, right, since all staff should have that?
634 # 2) It returns only one reservation at a time, even if an item can be
635 # and is currently overbooked. Hmmm....
636 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
637 my $result = $booking_ses->request(
638 "open-ils.booking.reservations.by_returnable_resource_barcode",
639 $self->editor->authtoken,
642 $booking_ses->disconnect;
644 return $self->bail_on_events($result) if defined $U->event_code($result);
647 $self->reservation(shift @$result);
655 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
656 sub save_trimmed_copy {
657 my ($self, $copy) = @_;
660 $self->volume($copy->call_number);
661 $self->title($self->volume->record);
662 $self->copy->call_number($self->volume->id);
663 $self->volume->record($self->title->id);
664 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
665 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
666 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
667 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
673 my $e = $self->editor;
675 # --------------------------------------------------------------------------
676 # Grab the fleshed copy
677 # --------------------------------------------------------------------------
678 unless($self->is_noncat) {
681 $copy = $e->retrieve_asset_copy(
682 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
684 } elsif( $self->copy_barcode ) {
686 $copy = $e->search_asset_copy(
687 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
688 } elsif( $self->reservation ) {
689 my $res = $e->json_query(
691 "select" => {"acp" => ["id"]},
696 "field" => "barcode",
700 "field" => "current_resource"
708 "id" => (ref $self->reservation) ?
709 $self->reservation->id : $self->reservation
714 if (ref $res eq "ARRAY" and scalar @$res) {
715 $logger->info("circulator: mapped reservation " .
716 $self->reservation . " to copy " . $res->[0]->{"id"});
717 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
722 $self->save_trimmed_copy($copy);
724 # We can't renew if there is no copy
725 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
726 if $self->is_renewal;
731 # --------------------------------------------------------------------------
733 # --------------------------------------------------------------------------
737 flesh_fields => {au => [ qw/ card / ]}
740 if( $self->patron_id ) {
741 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
742 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
744 } elsif( $self->patron_barcode ) {
746 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
747 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
748 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
750 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
751 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
754 if( my $copy = $self->copy ) {
757 $flesh->{flesh_fields}->{circ} = ['usr'];
759 my $circ = $e->search_action_circulation([
760 {target_copy => $copy->id, checkin_time => undef}, $flesh
764 $patron = $circ->usr;
765 $circ->usr($patron->id); # de-flesh for consistency
771 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
772 unless $self->patron($patron) or $self->is_checkin;
774 unless($self->is_checkin) {
776 # Check for inactivity and patron reg. expiration
778 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
779 unless $U->is_true($patron->active);
781 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
782 unless $U->is_true($patron->card->active);
784 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
785 cleanse_ISO8601($patron->expire_date));
787 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
788 if( CORE::time > $expire->epoch ) ;
792 # --------------------------------------------------------------------------
793 # This builds the script runner environment and fetches most of the
795 # --------------------------------------------------------------------------
796 sub mk_script_runner {
802 qw/copy copy_barcode copy_id patron
803 patron_id patron_barcode volume title editor/;
805 # Translate our objects into the ScriptBuilder args hash
806 $$args{$_} = $self->$_() for @fields;
808 $args->{ignore_user_status} = 1 if $self->is_checkin;
809 $$args{fetch_patron_by_circ_copy} = 1;
810 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
812 if( my $pco = $self->pending_checkouts ) {
813 $logger->info("circulator: we were given a pending checkouts number of $pco");
814 $$args{patronItemsOut} = $pco;
817 # This fetches most of the objects we need
818 $self->script_runner(
819 OpenILS::Application::Circ::ScriptBuilder->build($args));
821 # Now we translate the ScriptBuilder objects back into self
822 $self->$_($$args{$_}) for @fields;
824 my @evts = @{$args->{_events}} if $args->{_events};
826 $logger->debug("circulator: script builder returned events: @evts") if @evts;
830 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
831 if(!$self->is_noncat and
833 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
837 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
838 return $self->bail_on_events(@e);
843 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
844 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
845 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
846 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
850 # We can't renew if there is no copy
851 return $self->bail_on_events(@evts) if
852 $self->is_renewal and !$self->copy;
854 # Set some circ-specific flags in the script environment
855 my $evt = "environment";
856 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
858 if( $self->is_noncat ) {
859 $self->script_runner->insert("$evt.isNonCat", 1);
860 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
863 if( $self->is_precat ) {
864 $self->script_runner->insert("environment.isPrecat", 1, 1);
867 $self->script_runner->add_path( $_ ) for @$script_libs;
872 # --------------------------------------------------------------------------
873 # Does the circ permit work
874 # --------------------------------------------------------------------------
878 $self->log_me("do_permit()");
880 unless( $self->editor->requestor->id == $self->patron->id ) {
881 return $self->bail_on_events($self->editor->event)
882 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
885 $self->check_captured_holds();
886 $self->do_copy_checks();
887 return if $self->bail_out;
888 $self->run_patron_permit_scripts();
889 $self->run_copy_permit_scripts()
890 unless $self->is_precat or $self->is_noncat;
891 $self->check_item_deposit_events();
892 $self->override_events();
893 return if $self->bail_out;
895 if($self->is_precat and not $self->request_precat) {
898 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
899 return $self->bail_out(1) unless $self->is_renewal;
903 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
906 sub check_item_deposit_events {
908 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
909 if $self->is_deposit and not $self->is_deposit_exempt;
910 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
911 if $self->is_rental and not $self->is_rental_exempt;
914 # returns true if the user is not required to pay deposits
915 sub is_deposit_exempt {
917 my $pid = (ref $self->patron->profile) ?
918 $self->patron->profile->id : $self->patron->profile;
919 my $groups = $U->ou_ancestor_setting_value(
920 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
921 for my $grp (@$groups) {
922 return 1 if $self->is_group_descendant($grp, $pid);
927 # returns true if the user is not required to pay rental fees
928 sub is_rental_exempt {
930 my $pid = (ref $self->patron->profile) ?
931 $self->patron->profile->id : $self->patron->profile;
932 my $groups = $U->ou_ancestor_setting_value(
933 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
934 for my $grp (@$groups) {
935 return 1 if $self->is_group_descendant($grp, $pid);
940 sub is_group_descendant {
941 my($self, $p_id, $c_id) = @_;
942 return 0 unless defined $p_id and defined $c_id;
943 return 1 if $c_id == $p_id;
944 while(my $grp = $user_groups{$c_id}) {
945 $c_id = $grp->parent;
946 return 0 unless defined $c_id;
947 return 1 if $c_id == $p_id;
952 sub check_captured_holds {
954 my $copy = $self->copy;
955 my $patron = $self->patron;
957 return undef unless $copy;
959 my $s = $U->copy_status($copy->status)->id;
960 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
961 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
963 # Item is on the holds shelf, make sure it's going to the right person
964 my $holds = $self->editor->search_action_hold_request(
967 current_copy => $copy->id ,
968 capture_time => { '!=' => undef },
969 cancel_time => undef,
970 fulfillment_time => undef
976 if( $holds and $$holds[0] ) {
977 return undef if $$holds[0]->usr == $patron->id;
980 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
982 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
988 my $copy = $self->copy;
991 my $stat = $U->copy_status($copy->status)->id;
993 # We cannot check out a copy if it is in-transit
994 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
995 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
998 $self->handle_claims_returned();
999 return if $self->bail_out;
1001 # no claims returned circ was found, check if there is any open circ
1002 unless( $self->is_renewal ) {
1004 my $circs = $self->editor->search_action_circulation(
1005 { target_copy => $copy->id, checkin_time => undef }
1008 if(my $old_circ = $circs->[0]) { # an open circ was found
1010 my $payload = {copy => $copy};
1012 if($old_circ->usr == $self->patron->id) {
1014 $payload->{old_circ} = $old_circ;
1016 # If there is an open circulation on the checkout item and an auto-renew
1017 # interval is defined, inform the caller that they should go
1018 # ahead and renew the item instead of warning about open circulations.
1020 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1022 'circ.checkout_auto_renew_age',
1026 if($auto_renew_intvl) {
1027 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1028 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1030 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1031 $payload->{auto_renew} = 1;
1036 return $self->bail_on_events(
1037 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1043 my $LEGACY_CIRC_EVENT_MAP = {
1044 'no_item' => 'ITEM_NOT_CATALOGED',
1045 'actor.usr.barred' => 'PATRON_BARRED',
1046 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1047 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1048 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1049 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1050 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1051 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1052 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1053 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1057 # ---------------------------------------------------------------------
1058 # This pushes any patron-related events into the list but does not
1059 # set bail_out for any events
1060 # ---------------------------------------------------------------------
1061 sub run_patron_permit_scripts {
1063 my $runner = $self->script_runner;
1064 my $patronid = $self->patron->id;
1068 if(!$self->legacy_script_support) {
1070 my $results = $self->run_indb_circ_test;
1071 unless($self->circ_test_success) {
1072 # no_item result is OK during noncat checkout
1073 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1074 push @allevents, $self->matrix_test_result_events;
1080 # ---------------------------------------------------------------------
1081 # # Now run the patron permit script
1082 # ---------------------------------------------------------------------
1083 $runner->load($self->circ_permit_patron);
1084 my $result = $runner->run or
1085 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1087 my $patron_events = $result->{events};
1089 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1090 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1091 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1092 $penalties = $penalties->{fatal_penalties};
1094 for my $pen (@$penalties) {
1095 my $event = OpenILS::Event->new($pen->name);
1096 $event->{desc} = $pen->label;
1097 push(@allevents, $event);
1100 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1104 $_->{payload} = $self->copy if
1105 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1108 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1110 $self->push_events(@allevents);
1113 sub matrix_test_result_codes {
1115 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1118 sub matrix_test_result_events {
1121 my $event = new OpenILS::Event(
1122 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1124 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1126 } (@{$self->matrix_test_result});
1129 sub run_indb_circ_test {
1131 return $self->matrix_test_result if $self->matrix_test_result;
1133 my $dbfunc = ($self->is_renewal) ?
1134 'action.item_user_renew_test' : 'action.item_user_circ_test';
1136 if( $self->is_precat && $self->request_precat) {
1137 $self->make_precat_copy;
1138 return if $self->bail_out;
1141 my $results = $self->editor->json_query(
1145 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1151 $self->circ_test_success($U->is_true($results->[0]->{success}));
1153 if(my $mp = $results->[0]->{matchpoint}) {
1154 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1155 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1156 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1157 if($results->[0]->{renewals}) {
1158 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1160 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1161 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1162 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1165 return $self->matrix_test_result($results);
1168 # ---------------------------------------------------------------------
1169 # given a use and copy, this will calculate the circulation policy
1170 # parameters. Only works with in-db circ.
1171 # ---------------------------------------------------------------------
1175 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1177 $self->run_indb_circ_test;
1180 circ_test_success => $self->circ_test_success,
1181 failure_events => [],
1182 failure_codes => [],
1183 matchpoint => $self->circ_matrix_matchpoint
1186 unless($self->circ_test_success) {
1187 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1188 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1191 if($self->circ_matrix_matchpoint) {
1192 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1193 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1194 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1195 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1197 my $policy = $self->get_circ_policy(
1198 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1200 $$results{$_} = $$policy{$_} for keys %$policy;
1206 # ---------------------------------------------------------------------
1207 # Loads the circ policy info for duration, recurring fine, and max
1208 # fine based on the current copy
1209 # ---------------------------------------------------------------------
1210 sub get_circ_policy {
1211 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1214 duration_rule => $duration_rule->name,
1215 recurring_fine_rule => $recurring_fine_rule->name,
1216 max_fine_rule => $max_fine_rule->name,
1217 max_fine => $self->get_max_fine_amount($max_fine_rule),
1218 fine_interval => $recurring_fine_rule->recurrence_interval,
1219 renewal_remaining => $duration_rule->max_renewals
1222 if($hard_due_date) {
1223 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1224 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1227 $policy->{duration_date_ceiling} = undef;
1228 $policy->{duration_date_ceiling_force} = undef;
1231 $policy->{duration} = $duration_rule->shrt
1232 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1233 $policy->{duration} = $duration_rule->normal
1234 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1235 $policy->{duration} = $duration_rule->extended
1236 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1238 $policy->{recurring_fine} = $recurring_fine_rule->low
1239 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1240 $policy->{recurring_fine} = $recurring_fine_rule->normal
1241 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1242 $policy->{recurring_fine} = $recurring_fine_rule->high
1243 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1248 sub get_max_fine_amount {
1250 my $max_fine_rule = shift;
1251 my $max_amount = $max_fine_rule->amount;
1253 # if is_percent is true then the max->amount is
1254 # use as a percentage of the copy price
1255 if ($U->is_true($max_fine_rule->is_percent)) {
1256 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1257 $max_amount = $price * $max_fine_rule->amount / 100;
1259 $U->ou_ancestor_setting_value(
1261 'circ.max_fine.cap_at_price',
1265 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1266 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1274 sub run_copy_permit_scripts {
1276 my $copy = $self->copy || return;
1277 my $runner = $self->script_runner;
1281 if(!$self->legacy_script_support) {
1282 my $results = $self->run_indb_circ_test;
1283 push @allevents, $self->matrix_test_result_events
1284 unless $self->circ_test_success;
1287 # ---------------------------------------------------------------------
1288 # Capture all of the copy permit events
1289 # ---------------------------------------------------------------------
1290 $runner->load($self->circ_permit_copy);
1291 my $result = $runner->run or
1292 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1293 my $copy_events = $result->{events};
1295 # ---------------------------------------------------------------------
1296 # Now collect all of the events together
1297 # ---------------------------------------------------------------------
1298 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1301 # See if this copy has an alert message
1302 my $ae = $self->check_copy_alert();
1303 push( @allevents, $ae ) if $ae;
1305 # uniquify the events
1306 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1307 @allevents = values %hash;
1309 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1311 $self->push_events(@allevents);
1315 sub check_copy_alert {
1317 return undef if $self->is_renewal;
1318 return OpenILS::Event->new(
1319 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1320 if $self->copy and $self->copy->alert_message;
1326 # --------------------------------------------------------------------------
1327 # If the call is overriding and has permissions to override every collected
1328 # event, the are cleared. Any event that the caller does not have
1329 # permission to override, will be left in the event list and bail_out will
1331 # XXX We need code in here to cancel any holds/transits on copies
1332 # that are being force-checked out
1333 # --------------------------------------------------------------------------
1334 sub override_events {
1336 my @events = @{$self->events};
1337 return unless @events;
1339 if(!$self->override) {
1340 return $self->bail_out(1)
1341 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1346 for my $e (@events) {
1347 my $tc = $e->{textcode};
1348 next if $tc eq 'SUCCESS';
1349 my $ov = "$tc.override";
1350 $logger->info("circulator: attempting to override event: $ov");
1352 return $self->bail_on_events($self->editor->event)
1353 unless( $self->editor->allowed($ov) );
1358 # --------------------------------------------------------------------------
1359 # If there is an open claimsreturn circ on the requested copy, close the
1360 # circ if overriding, otherwise bail out
1361 # --------------------------------------------------------------------------
1362 sub handle_claims_returned {
1364 my $copy = $self->copy;
1366 my $CR = $self->editor->search_action_circulation(
1368 target_copy => $copy->id,
1369 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1370 checkin_time => undef,
1374 return unless ($CR = $CR->[0]);
1378 # - If the caller has set the override flag, we will check the item in
1379 if($self->override) {
1381 $CR->checkin_time('now');
1382 $CR->checkin_scan_time('now');
1383 $CR->checkin_lib($self->circ_lib);
1384 $CR->checkin_workstation($self->editor->requestor->wsid);
1385 $CR->checkin_staff($self->editor->requestor->id);
1387 $evt = $self->editor->event
1388 unless $self->editor->update_action_circulation($CR);
1391 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1394 $self->bail_on_events($evt) if $evt;
1399 # --------------------------------------------------------------------------
1400 # This performs the checkout
1401 # --------------------------------------------------------------------------
1405 $self->log_me("do_checkout()");
1407 # make sure perms are good if this isn't a renewal
1408 unless( $self->is_renewal ) {
1409 return $self->bail_on_events($self->editor->event)
1410 unless( $self->editor->allowed('COPY_CHECKOUT') );
1413 # verify the permit key
1414 unless( $self->check_permit_key ) {
1415 if( $self->permit_override ) {
1416 return $self->bail_on_events($self->editor->event)
1417 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1419 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1423 # if this is a non-cataloged circ, build the circ and finish
1424 if( $self->is_noncat ) {
1425 $self->checkout_noncat;
1427 OpenILS::Event->new('SUCCESS',
1428 payload => { noncat_circ => $self->circ }));
1432 if( $self->is_precat ) {
1433 $self->make_precat_copy;
1434 return if $self->bail_out;
1436 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1437 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1440 $self->do_copy_checks;
1441 return if $self->bail_out;
1443 $self->run_checkout_scripts();
1444 return if $self->bail_out;
1446 $self->build_checkout_circ_object();
1447 return if $self->bail_out;
1449 my $modify_to_start = $self->booking_adjusted_due_date();
1450 return if $self->bail_out;
1452 $self->apply_modified_due_date($modify_to_start);
1453 return if $self->bail_out;
1455 return $self->bail_on_events($self->editor->event)
1456 unless $self->editor->create_action_circulation($self->circ);
1458 # refresh the circ to force local time zone for now
1459 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1461 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1463 return if $self->bail_out;
1465 $self->apply_deposit_fee();
1466 return if $self->bail_out;
1468 $self->handle_checkout_holds();
1469 return if $self->bail_out;
1471 # ------------------------------------------------------------------------------
1472 # Update the patron penalty info in the DB. Run it for permit-overrides
1473 # since the penalties are not updated during the permit phase
1474 # ------------------------------------------------------------------------------
1475 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1477 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1480 if($self->is_renewal) {
1481 # flesh the billing summary for the checked-in circ
1482 $pcirc = $self->editor->retrieve_action_circulation([
1484 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1489 OpenILS::Event->new('SUCCESS',
1491 copy => $U->unflesh_copy($self->copy),
1492 circ => $self->circ,
1494 holds_fulfilled => $self->fulfilled_holds,
1495 deposit_billing => $self->deposit_billing,
1496 rental_billing => $self->rental_billing,
1497 parent_circ => $pcirc,
1498 patron => ($self->return_patron) ? $self->patron : undef,
1499 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1505 sub apply_deposit_fee {
1507 my $copy = $self->copy;
1509 ($self->is_deposit and not $self->is_deposit_exempt) or
1510 ($self->is_rental and not $self->is_rental_exempt);
1512 return if $self->is_deposit and $self->skip_deposit_fee;
1513 return if $self->is_rental and $self->skip_rental_fee;
1515 my $bill = Fieldmapper::money::billing->new;
1516 my $amount = $copy->deposit_amount;
1520 if($self->is_deposit) {
1521 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1523 $self->deposit_billing($bill);
1525 $billing_type = OILS_BILLING_TYPE_RENTAL;
1527 $self->rental_billing($bill);
1530 $bill->xact($self->circ->id);
1531 $bill->amount($amount);
1532 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1533 $bill->billing_type($billing_type);
1534 $bill->btype($btype);
1535 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1537 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1542 my $copy = $self->copy;
1544 my $stat = $copy->status if ref $copy->status;
1545 my $loc = $copy->location if ref $copy->location;
1546 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1548 $copy->status($stat->id) if $stat;
1549 $copy->location($loc->id) if $loc;
1550 $copy->circ_lib($circ_lib->id) if $circ_lib;
1551 $copy->editor($self->editor->requestor->id);
1552 $copy->edit_date('now');
1553 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1555 return $self->bail_on_events($self->editor->event)
1556 unless $self->editor->update_asset_copy($self->copy);
1558 $copy->status($U->copy_status($copy->status));
1559 $copy->location($loc) if $loc;
1560 $copy->circ_lib($circ_lib) if $circ_lib;
1563 sub update_reservation {
1565 my $reservation = $self->reservation;
1567 my $usr = $reservation->usr;
1568 my $target_rt = $reservation->target_resource_type;
1569 my $target_r = $reservation->target_resource;
1570 my $current_r = $reservation->current_resource;
1572 $reservation->usr($usr->id) if ref $usr;
1573 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1574 $reservation->target_resource($target_r->id) if ref $target_r;
1575 $reservation->current_resource($current_r->id) if ref $current_r;
1577 return $self->bail_on_events($self->editor->event)
1578 unless $self->editor->update_booking_reservation($self->reservation);
1581 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1582 $self->reservation($reservation);
1586 sub bail_on_events {
1587 my( $self, @evts ) = @_;
1588 $self->push_events(@evts);
1593 # ------------------------------------------------------------------------------
1594 # When an item is checked out, see if we can fulfill a hold for this patron
1595 # ------------------------------------------------------------------------------
1596 sub handle_checkout_holds {
1598 my $copy = $self->copy;
1599 my $patron = $self->patron;
1601 my $e = $self->editor;
1602 $self->fulfilled_holds([]);
1604 # pre/non-cats can't fulfill a hold
1605 return if $self->is_precat or $self->is_noncat;
1607 my $hold = $e->search_action_hold_request({
1608 current_copy => $copy->id ,
1609 cancel_time => undef,
1610 fulfillment_time => undef,
1612 {expire_time => undef},
1613 {expire_time => {'>' => 'now'}}
1617 if($hold and $hold->usr != $patron->id) {
1618 # reset the hold since the copy is now checked out
1620 $logger->info("circulator: un-targeting hold ".$hold->id.
1621 " because copy ".$copy->id." is getting checked out");
1623 $hold->clear_prev_check_time;
1624 $hold->clear_current_copy;
1625 $hold->clear_capture_time;
1627 return $self->bail_on_event($e->event)
1628 unless $e->update_action_hold_request($hold);
1634 $hold = $self->find_related_user_hold($copy, $patron) or return;
1635 $logger->info("circulator: found related hold to fulfill in checkout");
1638 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1640 # if the hold was never officially captured, capture it.
1641 $hold->current_copy($copy->id);
1642 $hold->capture_time('now') unless $hold->capture_time;
1643 $hold->fulfillment_time('now');
1644 $hold->fulfillment_staff($e->requestor->id);
1645 $hold->fulfillment_lib($self->circ_lib);
1647 return $self->bail_on_events($e->event)
1648 unless $e->update_action_hold_request($hold);
1650 $holdcode->delete_hold_copy_maps($e, $hold->id);
1651 return $self->fulfilled_holds([$hold->id]);
1655 # ------------------------------------------------------------------------------
1656 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1657 # the patron directly targets the checked out item, see if there is another hold
1658 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1659 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1660 # ------------------------------------------------------------------------------
1661 sub find_related_user_hold {
1662 my($self, $copy, $patron) = @_;
1663 my $e = $self->editor;
1665 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1667 return undef unless $U->ou_ancestor_setting_value(
1668 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1670 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1672 select => {ahr => ['id']},
1677 fkey => 'current_copy',
1678 type => 'left' # there may be no current_copy
1685 fulfillment_time => undef,
1686 cancel_time => undef,
1688 {expire_time => undef},
1689 {expire_time => {'>' => 'now'}}
1696 target => $self->volume->id
1702 target => $self->title->id
1708 {id => undef}, # left-join copy may be nonexistent
1709 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1713 order_by => {ahr => {request_time => {direction => 'asc'}}},
1717 my $hold_info = $e->json_query($args)->[0];
1718 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1723 sub run_checkout_scripts {
1728 my $runner = $self->script_runner;
1737 my $hard_due_date_name;
1739 if(!$self->legacy_script_support) {
1740 $self->run_indb_circ_test();
1741 $duration = $self->circ_matrix_matchpoint->duration_rule;
1742 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1743 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1744 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1748 $runner->load($self->circ_duration);
1750 my $result = $runner->run or
1751 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1753 $duration_name = $result->{durationRule};
1754 $recurring_name = $result->{recurringFinesRule};
1755 $max_fine_name = $result->{maxFine};
1756 $hard_due_date_name = $result->{hardDueDate};
1759 $duration_name = $duration->name if $duration;
1760 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1763 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1764 return $self->bail_on_events($evt) if ($evt && !$nobail);
1766 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1767 return $self->bail_on_events($evt) if ($evt && !$nobail);
1769 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1770 return $self->bail_on_events($evt) if ($evt && !$nobail);
1772 if($hard_due_date_name) {
1773 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1774 return $self->bail_on_events($evt) if ($evt && !$nobail);
1780 # The item circulates with an unlimited duration
1784 $hard_due_date = undef;
1787 $self->duration_rule($duration);
1788 $self->recurring_fines_rule($recurring);
1789 $self->max_fine_rule($max_fine);
1790 $self->hard_due_date($hard_due_date);
1794 sub build_checkout_circ_object {
1797 my $circ = Fieldmapper::action::circulation->new;
1798 my $duration = $self->duration_rule;
1799 my $max = $self->max_fine_rule;
1800 my $recurring = $self->recurring_fines_rule;
1801 my $hard_due_date = $self->hard_due_date;
1802 my $copy = $self->copy;
1803 my $patron = $self->patron;
1804 my $duration_date_ceiling;
1805 my $duration_date_ceiling_force;
1809 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1810 $duration_date_ceiling = $policy->{duration_date_ceiling};
1811 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1813 my $dname = $duration->name;
1814 my $mname = $max->name;
1815 my $rname = $recurring->name;
1817 if($hard_due_date) {
1818 $hdname = $hard_due_date->name;
1821 $logger->debug("circulator: building circulation ".
1822 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1824 $circ->duration($policy->{duration});
1825 $circ->recurring_fine($policy->{recurring_fine});
1826 $circ->duration_rule($duration->name);
1827 $circ->recurring_fine_rule($recurring->name);
1828 $circ->max_fine_rule($max->name);
1829 $circ->max_fine($policy->{max_fine});
1830 $circ->fine_interval($recurring->recurrence_interval);
1831 $circ->renewal_remaining($duration->max_renewals);
1835 $logger->info("circulator: copy found with an unlimited circ duration");
1836 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1837 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1838 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1839 $circ->renewal_remaining(0);
1842 $circ->target_copy( $copy->id );
1843 $circ->usr( $patron->id );
1844 $circ->circ_lib( $self->circ_lib );
1845 $circ->workstation($self->editor->requestor->wsid)
1846 if defined $self->editor->requestor->wsid;
1848 # renewals maintain a link to the parent circulation
1849 $circ->parent_circ($self->parent_circ);
1851 if( $self->is_renewal ) {
1852 $circ->opac_renewal('t') if $self->opac_renewal;
1853 $circ->phone_renewal('t') if $self->phone_renewal;
1854 $circ->desk_renewal('t') if $self->desk_renewal;
1855 $circ->renewal_remaining($self->renewal_remaining);
1856 $circ->circ_staff($self->editor->requestor->id);
1860 # if the user provided an overiding checkout time,
1861 # (e.g. the checkout really happened several hours ago), then
1862 # we apply that here. Does this need a perm??
1863 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1864 if $self->checkout_time;
1866 # if a patron is renewing, 'requestor' will be the patron
1867 $circ->circ_staff($self->editor->requestor->id);
1868 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1873 sub do_reservation_pickup {
1876 $self->log_me("do_reservation_pickup()");
1878 $self->reservation->pickup_time('now');
1881 $self->reservation->current_resource &&
1882 $U->is_true($self->reservation->target_resource_type->catalog_item)
1884 # We used to try to set $self->copy and $self->patron here,
1885 # but that should already be done.
1887 $self->run_checkout_scripts(1);
1889 my $duration = $self->duration_rule;
1890 my $max = $self->max_fine_rule;
1891 my $recurring = $self->recurring_fines_rule;
1893 if ($duration && $max && $recurring) {
1894 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1896 my $dname = $duration->name;
1897 my $mname = $max->name;
1898 my $rname = $recurring->name;
1900 $logger->debug("circulator: updating reservation ".
1901 "with duration=$dname, maxfine=$mname, recurring=$rname");
1903 $self->reservation->fine_amount($policy->{recurring_fine});
1904 $self->reservation->max_fine($policy->{max_fine});
1905 $self->reservation->fine_interval($recurring->recurrence_interval);
1908 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1909 $self->update_copy();
1912 $self->reservation->fine_amount(
1913 $self->reservation->target_resource_type->fine_amount
1915 $self->reservation->max_fine(
1916 $self->reservation->target_resource_type->max_fine
1918 $self->reservation->fine_interval(
1919 $self->reservation->target_resource_type->fine_interval
1923 $self->update_reservation();
1926 sub do_reservation_return {
1928 my $request = shift;
1930 $self->log_me("do_reservation_return()");
1932 if (not ref $self->reservation) {
1933 my ($reservation, $evt) =
1934 $U->fetch_booking_reservation($self->reservation);
1935 return $self->bail_on_events($evt) if $evt;
1936 $self->reservation($reservation);
1939 $self->generate_fines(1);
1940 $self->reservation->return_time('now');
1941 $self->update_reservation();
1942 $self->reshelve_copy if $self->copy;
1944 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1945 $self->copy( $self->reservation->current_resource->catalog_item );
1949 sub booking_adjusted_due_date {
1951 my $circ = $self->circ;
1952 my $copy = $self->copy;
1954 return undef unless $self->use_booking;
1958 if( $self->due_date ) {
1960 return $self->bail_on_events($self->editor->event)
1961 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1963 $circ->due_date(cleanse_ISO8601($self->due_date));
1967 return unless $copy and $circ->due_date;
1970 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1971 if (@$booking_items) {
1972 my $booking_item = $booking_items->[0];
1973 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1975 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1976 my $shorten_circ_setting = $resource_type->elbow_room ||
1977 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1980 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1981 my $bookings = $booking_ses->request(
1982 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1983 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1985 $booking_ses->disconnect;
1987 my $dt_parser = DateTime::Format::ISO8601->new;
1988 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1990 for my $bid (@$bookings) {
1992 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1994 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1995 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1997 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1998 if ($booking_start < DateTime->now);
2001 if ($U->is_true($stop_circ_setting)) {
2002 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2004 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2005 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2008 # We set the circ duration here only to affect the logic that will
2009 # later (in a DB trigger) mangle the time part of the due date to
2010 # 11:59pm. Having any circ duration that is not a whole number of
2011 # days is enough to prevent the "correction."
2012 my $new_circ_duration = $due_date->epoch - time;
2013 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2014 $circ->duration("$new_circ_duration seconds");
2016 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2020 return $self->bail_on_events($self->editor->event)
2021 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2027 sub apply_modified_due_date {
2029 my $shift_earlier = shift;
2030 my $circ = $self->circ;
2031 my $copy = $self->copy;
2033 if( $self->due_date ) {
2035 return $self->bail_on_events($self->editor->event)
2036 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2038 $circ->due_date(cleanse_ISO8601($self->due_date));
2042 # if the due_date lands on a day when the location is closed
2043 return unless $copy and $circ->due_date;
2045 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2047 # due-date overlap should be determined by the location the item
2048 # is checked out from, not the owning or circ lib of the item
2049 my $org = $self->circ_lib;
2051 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2052 " with an item due date of ".$circ->due_date );
2054 my $dateinfo = $U->storagereq(
2055 'open-ils.storage.actor.org_unit.closed_date.overlap',
2056 $org, $circ->due_date );
2059 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2060 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2062 # XXX make the behavior more dynamic
2063 # for now, we just push the due date to after the close date
2064 if ($shift_earlier) {
2065 $circ->due_date($dateinfo->{start});
2067 $circ->due_date($dateinfo->{end});
2075 sub create_due_date {
2076 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2078 # if there is a raw time component (e.g. from postgres),
2079 # turn it into an interval that interval_to_seconds can parse
2080 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2082 # for now, use the server timezone. TODO: use workstation org timezone
2083 my $due_date = DateTime->now(time_zone => 'local');
2085 # add the circ duration
2086 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2089 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2090 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2091 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2096 # return ISO8601 time with timezone
2097 return $due_date->strftime('%FT%T%z');
2102 sub make_precat_copy {
2104 my $copy = $self->copy;
2107 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2109 $copy->editor($self->editor->requestor->id);
2110 $copy->edit_date('now');
2111 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2112 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2113 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2114 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2115 $self->update_copy();
2119 $logger->info("circulator: Creating a new precataloged ".
2120 "copy in checkout with barcode " . $self->copy_barcode);
2122 $copy = Fieldmapper::asset::copy->new;
2123 $copy->circ_lib($self->circ_lib);
2124 $copy->creator($self->editor->requestor->id);
2125 $copy->editor($self->editor->requestor->id);
2126 $copy->barcode($self->copy_barcode);
2127 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2128 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2129 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2131 $copy->dummy_title($self->dummy_title || "");
2132 $copy->dummy_author($self->dummy_author || "");
2133 $copy->dummy_isbn($self->dummy_isbn || "");
2134 $copy->circ_modifier($self->circ_modifier);
2137 # See if we need to override the circ_lib for the copy with a configured circ_lib
2138 # Setting is shortname of the org unit
2139 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2140 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2142 if($precat_circ_lib) {
2143 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2146 $self->bail_on_events($self->editor->event);
2150 $copy->circ_lib($org->id);
2154 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2156 $self->push_events($self->editor->event);
2160 # this is a little bit of a hack, but we need to
2161 # get the copy into the script runner
2162 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2166 sub checkout_noncat {
2172 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2173 my $count = $self->noncat_count || 1;
2174 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2176 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2180 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2181 $self->editor->requestor->id,
2189 $self->push_events($evt);
2200 $self->log_me("do_checkin()");
2202 return $self->bail_on_events(
2203 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2206 # the renew code and mk_env should have already found our circulation object
2207 unless( $self->circ ) {
2209 my $circs = $self->editor->search_action_circulation(
2210 { target_copy => $self->copy->id, checkin_time => undef });
2212 $self->circ($$circs[0]);
2214 # for now, just warn if there are multiple open circs on a copy
2215 $logger->warn("circulator: we have ".scalar(@$circs).
2216 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2219 # run the fine generator against this circ, if this circ is there
2220 $self->generate_fines_start if $self->circ;
2222 if( $self->checkin_check_holds_shelf() ) {
2223 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2224 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2225 $self->checkin_flesh_events;
2229 unless( $self->is_renewal ) {
2230 return $self->bail_on_events($self->editor->event)
2231 unless $self->editor->allowed('COPY_CHECKIN');
2234 $self->push_events($self->check_copy_alert());
2235 $self->push_events($self->check_checkin_copy_status());
2237 # if the circ is marked as 'claims returned', add the event to the list
2238 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2239 if ($self->circ and $self->circ->stop_fines
2240 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2242 $self->check_circ_deposit();
2244 # handle the overridable events
2245 $self->override_events unless $self->is_renewal;
2246 return if $self->bail_out;
2250 $self->editor->search_action_transit_copy(
2251 { target_copy => $self->copy->id, dest_recv_time => undef }
2257 $self->checkin_handle_circ;
2258 return if $self->bail_out;
2259 $self->checkin_changed(1);
2261 } elsif( $self->transit ) {
2262 my $hold_transit = $self->process_received_transit;
2263 $self->checkin_changed(1);
2265 if( $self->bail_out ) {
2266 $self->checkin_flesh_events;
2270 if( my $e = $self->check_checkin_copy_status() ) {
2271 # If the original copy status is special, alert the caller
2272 my $ev = $self->events;
2273 $self->events([$e]);
2274 $self->override_events;
2275 return if $self->bail_out;
2279 if( $hold_transit or
2280 $U->copy_status($self->copy->status)->id
2281 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2284 if( $hold_transit ) {
2285 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2287 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2292 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2294 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2295 $self->reshelve_copy(1);
2296 $self->cancelled_hold_transit(1);
2297 $self->notify_hold(0); # don't notify for cancelled holds
2298 return if $self->bail_out;
2302 # hold transited to correct location
2303 $self->checkin_flesh_events;
2308 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2310 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2311 " that is in-transit, but there is no transit.. repairing");
2312 $self->reshelve_copy(1);
2313 return if $self->bail_out;
2316 if( $self->is_renewal ) {
2317 $self->finish_fines_and_voiding;
2318 return if $self->bail_out;
2319 $self->push_events(OpenILS::Event->new('SUCCESS'));
2323 # ------------------------------------------------------------------------------
2324 # Circulations and transits are now closed where necessary. Now go on to see if
2325 # this copy can fulfill a hold or needs to be routed to a different location
2326 # ------------------------------------------------------------------------------
2328 my $needed_for_something = 0; # formerly "needed_for_hold"
2330 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2332 if (!$self->remote_hold) {
2333 if ($self->use_booking) {
2334 my $potential_hold = $self->hold_capture_is_possible;
2335 my $potential_reservation = $self->reservation_capture_is_possible;
2337 if ($potential_hold and $potential_reservation) {
2338 $logger->info("circulator: item could fulfill either hold or reservation");
2339 $self->push_events(new OpenILS::Event(
2340 "HOLD_RESERVATION_CONFLICT",
2341 "hold" => $potential_hold,
2342 "reservation" => $potential_reservation
2344 return if $self->bail_out;
2345 } elsif ($potential_hold) {
2346 $needed_for_something =
2347 $self->attempt_checkin_hold_capture;
2348 } elsif ($potential_reservation) {
2349 $needed_for_something =
2350 $self->attempt_checkin_reservation_capture;
2353 $needed_for_something = $self->attempt_checkin_hold_capture;
2356 return if $self->bail_out;
2358 unless($needed_for_something) {
2359 my $circ_lib = (ref $self->copy->circ_lib) ?
2360 $self->copy->circ_lib->id : $self->copy->circ_lib;
2362 if( $self->remote_hold ) {
2363 $circ_lib = $self->remote_hold->pickup_lib;
2364 $logger->warn("circulator: Copy ".$self->copy->barcode.
2365 " is on a remote hold's shelf, sending to $circ_lib");
2368 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2370 if( $circ_lib == $self->circ_lib) {
2371 # copy is where it needs to be, either for hold or reshelving
2373 $self->checkin_handle_precat();
2374 return if $self->bail_out;
2377 # copy needs to transit "home", or stick here if it's a floating copy
2379 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2380 $self->checkin_changed(1);
2381 $self->copy->circ_lib( $self->circ_lib );
2384 my $bc = $self->copy->barcode;
2385 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2386 $self->checkin_build_copy_transit($circ_lib);
2387 return if $self->bail_out;
2388 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2392 } else { # no-op checkin
2393 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2394 $self->checkin_changed(1);
2395 $self->copy->circ_lib( $self->circ_lib );
2400 if($self->claims_never_checked_out and
2401 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2403 # the item was not supposed to be checked out to the user and should now be marked as missing
2404 $self->copy->status(OILS_COPY_STATUS_MISSING);
2408 $self->reshelve_copy unless $needed_for_something;
2411 return if $self->bail_out;
2413 unless($self->checkin_changed) {
2415 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2416 my $stat = $U->copy_status($self->copy->status)->id;
2418 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2419 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2420 $self->bail_out(1); # no need to commit anything
2424 $self->push_events(OpenILS::Event->new('SUCCESS'))
2425 unless @{$self->events};
2428 $self->finish_fines_and_voiding;
2430 OpenILS::Utils::Penalty->calculate_penalties(
2431 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2433 $self->checkin_flesh_events;
2437 sub finish_fines_and_voiding {
2439 return unless $self->circ;
2441 # gather any updates to the circ after fine generation, if there was a circ
2442 $self->generate_fines_finish;
2444 return unless $self->backdate or $self->void_overdues;
2446 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2447 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2449 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2450 $self->editor, $self->circ, $self->backdate, $note);
2452 return $self->bail_on_events($evt) if $evt;
2454 # make sure the circ isn't closed if we just voided some fines
2455 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2456 return $self->bail_on_events($evt) if $evt;
2462 # if a deposit was payed for this item, push the event
2463 sub check_circ_deposit {
2465 return unless $self->circ;
2466 my $deposit = $self->editor->search_money_billing(
2468 xact => $self->circ->id,
2470 }, {idlist => 1})->[0];
2472 $self->push_events(OpenILS::Event->new(
2473 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2478 my $force = $self->force || shift;
2479 my $copy = $self->copy;
2481 my $stat = $U->copy_status($copy->status)->id;
2484 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2485 $stat != OILS_COPY_STATUS_CATALOGING and
2486 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2487 $stat != OILS_COPY_STATUS_RESHELVING )) {
2489 $copy->status( OILS_COPY_STATUS_RESHELVING );
2491 $self->checkin_changed(1);
2496 # Returns true if the item is at the current location
2497 # because it was transited there for a hold and the
2498 # hold has not been fulfilled
2499 sub checkin_check_holds_shelf {
2501 return 0 unless $self->copy;
2504 $U->copy_status($self->copy->status)->id ==
2505 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2507 # find the hold that put us on the holds shelf
2508 my $holds = $self->editor->search_action_hold_request(
2510 current_copy => $self->copy->id,
2511 capture_time => { '!=' => undef },
2512 fulfillment_time => undef,
2513 cancel_time => undef,
2518 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2519 $self->reshelve_copy(1);
2523 my $hold = $$holds[0];
2525 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2526 $hold->id. "] for copy ".$self->copy->barcode);
2528 if( $hold->pickup_lib == $self->circ_lib ) {
2529 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2533 $logger->info("circulator: hold is not for here..");
2534 $self->remote_hold($hold);
2539 sub checkin_handle_precat {
2541 my $copy = $self->copy;
2543 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2544 $copy->status(OILS_COPY_STATUS_CATALOGING);
2545 $self->update_copy();
2546 $self->checkin_changed(1);
2547 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2552 sub checkin_build_copy_transit {
2555 my $copy = $self->copy;
2556 my $transit = Fieldmapper::action::transit_copy->new;
2558 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2559 $logger->info("circulator: transiting copy to $dest");
2561 $transit->source($self->circ_lib);
2562 $transit->dest($dest);
2563 $transit->target_copy($copy->id);
2564 $transit->source_send_time('now');
2565 $transit->copy_status( $U->copy_status($copy->status)->id );
2567 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2569 return $self->bail_on_events($self->editor->event)
2570 unless $self->editor->create_action_transit_copy($transit);
2572 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2574 $self->checkin_changed(1);
2578 sub hold_capture_is_possible {
2580 my $copy = $self->copy;
2582 # we've been explicitly told not to capture any holds
2583 return 0 if $self->capture eq 'nocapture';
2585 # See if this copy can fulfill any holds
2586 my $hold = $holdcode->find_nearest_permitted_hold(
2587 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2589 return undef if ref $hold eq "HASH" and
2590 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2594 sub reservation_capture_is_possible {
2596 my $copy = $self->copy;
2598 # we've been explicitly told not to capture any holds
2599 return 0 if $self->capture eq 'nocapture';
2601 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2602 my $resv = $booking_ses->request(
2603 "open-ils.booking.reservations.could_capture",
2604 $self->editor->authtoken, $copy->barcode
2606 $booking_ses->disconnect;
2607 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2608 $self->push_events($resv);
2614 # returns true if the item was used (or may potentially be used
2615 # in subsequent calls) to capture a hold.
2616 sub attempt_checkin_hold_capture {
2618 my $copy = $self->copy;
2620 # we've been explicitly told not to capture any holds
2621 return 0 if $self->capture eq 'nocapture';
2623 # See if this copy can fulfill any holds
2624 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2625 $self->editor, $copy, $self->editor->requestor );
2628 $logger->debug("circulator: no potential permitted".
2629 "holds found for copy ".$copy->barcode);
2633 if($self->capture ne 'capture') {
2634 # see if this item is in a hold-capture-delay location
2635 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2636 if($U->is_true($location->hold_verify)) {
2637 $self->bail_on_events(
2638 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2643 $self->retarget($retarget);
2645 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2647 $hold->current_copy($copy->id);
2648 $hold->capture_time('now');
2649 $self->put_hold_on_shelf($hold)
2650 if $hold->pickup_lib == $self->circ_lib;
2652 # prevent DB errors caused by fetching
2653 # holds from storage, and updating through cstore
2654 $hold->clear_fulfillment_time;
2655 $hold->clear_fulfillment_staff;
2656 $hold->clear_fulfillment_lib;
2657 $hold->clear_expire_time;
2658 $hold->clear_cancel_time;
2659 $hold->clear_prev_check_time unless $hold->prev_check_time;
2661 $self->bail_on_events($self->editor->event)
2662 unless $self->editor->update_action_hold_request($hold);
2664 $self->checkin_changed(1);
2666 return 0 if $self->bail_out;
2668 if( $hold->pickup_lib == $self->circ_lib ) {
2670 # This hold was captured in the correct location
2671 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2672 $self->push_events(OpenILS::Event->new('SUCCESS'));
2674 #$self->do_hold_notify($hold->id);
2675 $self->notify_hold($hold->id);
2679 # Hold needs to be picked up elsewhere. Build a hold
2680 # transit and route the item.
2681 $self->checkin_build_hold_transit();
2682 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2683 return 0 if $self->bail_out;
2684 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2687 # make sure we save the copy status
2692 sub attempt_checkin_reservation_capture {
2694 my $copy = $self->copy;
2696 # we've been explicitly told not to capture any holds
2697 return 0 if $self->capture eq 'nocapture';
2699 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2700 my $evt = $booking_ses->request(
2701 "open-ils.booking.resources.capture_for_reservation",
2702 $self->editor->authtoken,
2704 1 # don't update copy - we probably have it locked
2706 $booking_ses->disconnect;
2708 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2710 "open-ils.booking.resources.capture_for_reservation " .
2711 "didn't return an event!"
2715 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2716 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2718 # not-transferable is an error event we'll pass on the user
2719 $logger->warn("reservation capture attempted against non-transferable item");
2720 $self->push_events($evt);
2722 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2723 # Re-retrieve copy as reservation capture may have changed
2724 # its status and whatnot.
2726 "circulator: booking capture win on copy " . $self->copy->id
2728 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2730 "circulator: changing copy " . $self->copy->id .
2731 "'s status from " . $self->copy->status . " to " .
2734 $self->copy->status($new_copy_status);
2737 $self->reservation($evt->{"payload"}->{"reservation"});
2739 if (exists $evt->{"payload"}->{"transit"}) {
2743 "org" => $evt->{"payload"}->{"transit"}->dest
2747 $self->checkin_changed(1);
2751 # other results are treated as "nothing to capture"
2755 sub do_hold_notify {
2756 my( $self, $holdid ) = @_;
2758 my $e = new_editor(xact => 1);
2759 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2761 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2762 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2764 $logger->info("circulator: running delayed hold notify process");
2766 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2767 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2769 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2770 hold_id => $holdid, requestor => $self->editor->requestor);
2772 $logger->debug("circulator: built hold notifier");
2774 if(!$notifier->event) {
2776 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2778 my $stat = $notifier->send_email_notify;
2779 if( $stat == '1' ) {
2780 $logger->info("circulator: hold notify succeeded for hold $holdid");
2784 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2787 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2791 sub retarget_holds {
2793 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2794 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2795 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2796 # no reason to wait for the return value
2800 sub checkin_build_hold_transit {
2803 my $copy = $self->copy;
2804 my $hold = $self->hold;
2805 my $trans = Fieldmapper::action::hold_transit_copy->new;
2807 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2809 $trans->hold($hold->id);
2810 $trans->source($self->circ_lib);
2811 $trans->dest($hold->pickup_lib);
2812 $trans->source_send_time("now");
2813 $trans->target_copy($copy->id);
2815 # when the copy gets to its destination, it will recover
2816 # this status - put it onto the holds shelf
2817 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2819 return $self->bail_on_events($self->editor->event)
2820 unless $self->editor->create_action_hold_transit_copy($trans);
2825 sub process_received_transit {
2827 my $copy = $self->copy;
2828 my $copyid = $self->copy->id;
2830 my $status_name = $U->copy_status($copy->status)->name;
2831 $logger->debug("circulator: attempting transit receive on ".
2832 "copy $copyid. Copy status is $status_name");
2834 my $transit = $self->transit;
2836 if( $transit->dest != $self->circ_lib ) {
2837 # - this item is in-transit to a different location
2839 my $tid = $transit->id;
2840 my $loc = $self->circ_lib;
2841 my $dest = $transit->dest;
2843 $logger->info("circulator: Fowarding transit on copy which is destined ".
2844 "for a different location. transit=$tid, copy=$copyid, current ".
2845 "location=$loc, destination location=$dest");
2847 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2849 # grab the associated hold object if available
2850 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2851 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2853 return $self->bail_on_events($evt);
2856 # The transit is received, set the receive time
2857 $transit->dest_recv_time('now');
2858 $self->bail_on_events($self->editor->event)
2859 unless $self->editor->update_action_transit_copy($transit);
2861 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2863 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2864 $copy->status( $transit->copy_status );
2865 $self->update_copy();
2866 return if $self->bail_out;
2870 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2872 # hold has arrived at destination, set shelf time
2873 $self->put_hold_on_shelf($hold);
2874 $self->bail_on_events($self->editor->event)
2875 unless $self->editor->update_action_hold_request($hold);
2876 return if $self->bail_out;
2878 $self->notify_hold($hold_transit->hold);
2883 OpenILS::Event->new(
2886 payload => { transit => $transit, holdtransit => $hold_transit } ));
2888 return $hold_transit;
2892 # ------------------------------------------------------------------
2893 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2894 # ------------------------------------------------------------------
2895 sub put_hold_on_shelf {
2896 my($self, $hold) = @_;
2898 $hold->shelf_time('now');
2900 my $shelf_expire = $U->ou_ancestor_setting_value(
2901 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2904 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2905 my $expire_time = DateTime->now->add(seconds => $seconds);
2906 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2914 sub generate_fines {
2916 my $reservation = shift;
2918 $self->generate_fines_start($reservation);
2919 $self->generate_fines_finish($reservation);
2924 sub generate_fines_start {
2926 my $reservation = shift;
2928 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2930 if (!exists($self->{_gen_fines_req})) {
2931 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
2933 'open-ils.storage.action.circulation.overdue.generate_fines',
2942 sub generate_fines_finish {
2944 my $reservation = shift;
2946 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2948 $self->{_gen_fines_req}->wait_complete;
2949 delete($self->{_gen_fines_req});
2951 # refresh the circ in case the fine generator set the stop_fines field
2952 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2953 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2958 sub checkin_handle_circ {
2960 my $circ = $self->circ;
2961 my $copy = $self->copy;
2965 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2967 # backdate the circ if necessary
2968 if($self->backdate) {
2969 my $evt = $self->checkin_handle_backdate;
2970 return $self->bail_on_events($evt) if $evt;
2973 if(!$circ->stop_fines) {
2974 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2975 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2976 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2977 $circ->stop_fines_time('now');
2978 $circ->stop_fines_time($self->backdate) if $self->backdate;
2981 # Set the checkin vars since we have the item
2982 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2984 # capture the true scan time for back-dated checkins
2985 $circ->checkin_scan_time('now');
2987 $circ->checkin_staff($self->editor->requestor->id);
2988 $circ->checkin_lib($self->circ_lib);
2989 $circ->checkin_workstation($self->editor->requestor->wsid);
2991 my $circ_lib = (ref $self->copy->circ_lib) ?
2992 $self->copy->circ_lib->id : $self->copy->circ_lib;
2993 my $stat = $U->copy_status($self->copy->status)->id;
2995 # immediately available keeps items lost or missing items from going home before being handled
2996 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2997 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3000 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3002 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3003 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3005 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3009 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3011 $self->checkin_handle_lost($circ_lib);
3015 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3020 # see if there are any fines owed on this circ. if not, close it
3021 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3022 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3024 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3026 return $self->bail_on_events($self->editor->event)
3027 unless $self->editor->update_action_circulation($circ);
3033 # ------------------------------------------------------------------
3034 # See if we need to void billings for lost checkin
3035 # ------------------------------------------------------------------
3036 sub checkin_handle_lost {
3038 my $circ_lib = shift;
3039 my $circ = $self->circ;
3041 my $max_return = $U->ou_ancestor_setting_value(
3042 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3047 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3048 $tm[5] -= 1 if $tm[5] > 0;
3049 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3051 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3052 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3054 $max_return = 0 if $today < $last_chance;
3057 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3059 my $void_lost = $U->ou_ancestor_setting_value(
3060 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3061 my $void_lost_fee = $U->ou_ancestor_setting_value(
3062 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3063 my $restore_od = $U->ou_ancestor_setting_value(
3064 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3066 $self->checkin_handle_lost_now_found(3) if $void_lost;
3067 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3068 $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3071 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3076 sub checkin_handle_backdate {
3079 # ------------------------------------------------------------------
3080 # clean up the backdate for date comparison
3081 # XXX We are currently taking the due-time from the original due-date,
3082 # not the input. Do we need to do this? This certainly interferes with
3083 # backdating of hourly checkouts, but that is likely a very rare case.
3084 # ------------------------------------------------------------------
3085 my $bd = cleanse_ISO8601($self->backdate);
3086 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3087 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3088 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3090 $self->backdate($bd);
3095 sub check_checkin_copy_status {
3097 my $copy = $self->copy;
3099 my $status = $U->copy_status($copy->status)->id;
3102 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3103 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3104 $status == OILS_COPY_STATUS_IN_PROCESS ||
3105 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3106 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3107 $status == OILS_COPY_STATUS_CATALOGING ||
3108 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3109 $status == OILS_COPY_STATUS_RESHELVING );
3111 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3112 if( $status == OILS_COPY_STATUS_LOST );
3114 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3115 if( $status == OILS_COPY_STATUS_MISSING );
3117 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3122 # --------------------------------------------------------------------------
3123 # On checkin, we need to return as many relevant objects as we can
3124 # --------------------------------------------------------------------------
3125 sub checkin_flesh_events {
3128 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3129 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3130 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3133 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3136 if($self->hold and !$self->hold->cancel_time) {
3137 $hold = $self->hold;
3138 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3142 # if we checked in a circulation, flesh the billing summary data
3143 $self->circ->billable_transaction(
3144 $self->editor->retrieve_money_billable_transaction([
3146 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3152 # flesh some patron fields before returning
3154 $self->editor->retrieve_actor_user([
3159 au => ['card', 'billing_address', 'mailing_address']
3166 for my $evt (@{$self->events}) {
3169 $payload->{copy} = $U->unflesh_copy($self->copy);
3170 $payload->{record} = $record,
3171 $payload->{circ} = $self->circ;
3172 $payload->{transit} = $self->transit;
3173 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3174 $payload->{hold} = $hold;
3175 $payload->{patron} = $self->patron;
3176 $payload->{reservation} = $self->reservation
3177 unless (not $self->reservation or $self->reservation->cancel_time);
3179 $evt->{payload} = $payload;
3184 my( $self, $msg ) = @_;
3185 my $bc = ($self->copy) ? $self->copy->barcode :
3188 my $usr = ($self->patron) ? $self->patron->id : "";
3189 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3190 ", recipient=$usr, copy=$bc");
3196 $self->log_me("do_renew()");
3198 # Make sure there is an open circ to renew that is not
3199 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3200 my $usrid = $self->patron->id if $self->patron;
3201 my $circ = $self->editor->search_action_circulation({
3202 target_copy => $self->copy->id,
3203 xact_finish => undef,
3204 ($usrid ? (usr => $usrid) : ()),
3206 {stop_fines => undef},
3207 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3211 return $self->bail_on_events($self->editor->event) unless $circ;
3213 # A user is not allowed to renew another user's items without permission
3214 unless( $circ->usr eq $self->editor->requestor->id ) {
3215 return $self->bail_on_events($self->editor->events)
3216 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3219 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3220 if $circ->renewal_remaining < 1;
3222 # -----------------------------------------------------------------
3224 $self->parent_circ($circ->id);
3225 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3228 # Run the fine generator against the old circ
3229 $self->generate_fines_start;
3231 $self->run_renew_permit;
3234 $self->do_checkin();
3235 return if $self->bail_out;
3237 unless( $self->permit_override ) {
3239 return if $self->bail_out;
3240 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3241 $self->remove_event('ITEM_NOT_CATALOGED');
3244 $self->override_events;
3245 return if $self->bail_out;
3248 $self->do_checkout();
3253 my( $self, $evt ) = @_;
3254 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3255 $logger->debug("circulator: removing event from list: $evt");
3256 my @events = @{$self->events};
3257 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3262 my( $self, $evt ) = @_;
3263 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3264 return grep { $_->{textcode} eq $evt } @{$self->events};
3269 sub run_renew_permit {
3272 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3273 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3274 $self->editor, $self->copy, $self->editor->requestor, 1
3276 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3279 if(!$self->legacy_script_support) {
3280 my $results = $self->run_indb_circ_test;
3281 $self->push_events($self->matrix_test_result_events)
3282 unless $self->circ_test_success;
3285 my $runner = $self->script_runner;
3287 $runner->load($self->circ_permit_renew);
3288 my $result = $runner->run or
3289 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3290 if ($result->{"events"}) {
3292 map { new OpenILS::Event($_) } @{$result->{"events"}}
3295 "circulator: circ_permit_renew for user " .
3296 $self->patron->id . " returned " .
3297 scalar(@{$result->{"events"}}) . " event(s)"
3301 $self->mk_script_runner;
3304 $logger->debug("circulator: re-creating script runner to be safe");
3308 # XXX: The primary mechanism for storing circ history is now handled
3309 # by tracking real circulation objects instead of bibs in a bucket.
3310 # However, this code is disabled by default and could be useful
3311 # some day, so may as well leave it for now.
3312 sub append_reading_list {
3316 $self->is_checkout and
3322 # verify history is globally enabled and uses the bucket mechanism
3323 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3324 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3326 return undef unless $htype and $htype eq 'bucket';
3328 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3330 # verify the patron wants to retain the hisory
3331 my $setting = $e->search_actor_user_setting(
3332 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3334 unless($setting and $setting->value) {
3339 my $bkt = $e->search_container_copy_bucket(
3340 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3345 # find the next item position
3346 my $last_item = $e->search_container_copy_bucket_item(
3347 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3348 $pos = $last_item->pos + 1 if $last_item;
3351 # create the history bucket if necessary
3352 $bkt = Fieldmapper::container::copy_bucket->new;
3353 $bkt->owner($self->patron->id);
3355 $bkt->btype('circ_history');
3357 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3360 my $item = Fieldmapper::container::copy_bucket_item->new;
3362 $item->bucket($bkt->id);
3363 $item->target_copy($self->copy->id);
3366 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3373 sub make_trigger_events {
3375 return unless $self->circ;
3376 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3377 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3378 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3383 sub checkin_handle_lost_now_found {
3384 my ($self, $bill_type) = @_;
3386 # ------------------------------------------------------------------
3387 # remove charge from patron's account if lost item is returned
3388 # ------------------------------------------------------------------
3390 my $bills = $self->editor->search_money_billing(
3392 xact => $self->circ->id,
3397 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3398 for my $bill (@$bills) {
3399 if( !$U->is_true($bill->voided) ) {
3400 $logger->info("lost item returned - voiding bill ".$bill->id);
3402 $bill->void_time('now');
3403 $bill->voider($self->editor->requestor->id);
3404 my $note = ($bill->note) ? $bill->note . "\n" : '';
3405 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3407 $self->bail_on_events($self->editor->event)
3408 unless $self->editor->update_money_billing($bill);
3413 sub checkin_handle_lost_now_found_restore_od {
3416 # ------------------------------------------------------------------
3417 # restore those overdue charges voided when item was set to lost
3418 # ------------------------------------------------------------------
3420 my $ods = $self->editor->search_money_billing(
3422 xact => $self->circ->id,
3427 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3428 for my $bill (@$ods) {
3429 if( $U->is_true($bill->voided) ) {
3430 $logger->info("lost item returned - restoring overdue ".$bill->id);
3432 $bill->clear_void_time;
3433 $bill->voider($self->editor->requestor->id);
3434 my $note = ($bill->note) ? $bill->note . "\n" : '';
3435 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3437 $self->bail_on_events($self->editor->event)
3438 unless $self->editor->update_money_billing($bill);