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 $mp");
1155 $self->circ_matrix_matchpoint(
1156 $self->editor->retrieve_config_circ_matrix_matchpoint([
1159 flesh_fields => {ccmm =>
1160 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule', 'hard_due_date']}
1166 return $self->matrix_test_result($results);
1169 # ---------------------------------------------------------------------
1170 # given a use and copy, this will calculate the circulation policy
1171 # parameters. Only works with in-db circ.
1172 # ---------------------------------------------------------------------
1176 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1178 $self->run_indb_circ_test;
1181 circ_test_success => $self->circ_test_success,
1182 failure_events => [],
1183 failure_codes => [],
1184 matchpoint => $self->circ_matrix_matchpoint
1187 unless($self->circ_test_success) {
1188 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1189 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1192 if($self->circ_matrix_matchpoint) {
1193 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1194 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1195 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1196 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1198 my $policy = $self->get_circ_policy(
1199 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1201 $$results{$_} = $$policy{$_} for keys %$policy;
1207 # ---------------------------------------------------------------------
1208 # Loads the circ policy info for duration, recurring fine, and max
1209 # fine based on the current copy
1210 # ---------------------------------------------------------------------
1211 sub get_circ_policy {
1212 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1215 duration_rule => $duration_rule->name,
1216 recurring_fine_rule => $recurring_fine_rule->name,
1217 max_fine_rule => $max_fine_rule->name,
1218 max_fine => $self->get_max_fine_amount($max_fine_rule),
1219 fine_interval => $recurring_fine_rule->recurrence_interval,
1220 renewal_remaining => $duration_rule->max_renewals
1223 if($hard_due_date) {
1224 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1225 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1228 $policy->{duration_date_ceiling} = undef;
1229 $policy->{duration_date_ceiling_force} = undef;
1232 $policy->{duration} = $duration_rule->shrt
1233 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1234 $policy->{duration} = $duration_rule->normal
1235 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1236 $policy->{duration} = $duration_rule->extended
1237 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1239 $policy->{recurring_fine} = $recurring_fine_rule->low
1240 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1241 $policy->{recurring_fine} = $recurring_fine_rule->normal
1242 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1243 $policy->{recurring_fine} = $recurring_fine_rule->high
1244 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1249 sub get_max_fine_amount {
1251 my $max_fine_rule = shift;
1252 my $max_amount = $max_fine_rule->amount;
1254 # if is_percent is true then the max->amount is
1255 # use as a percentage of the copy price
1256 if ($U->is_true($max_fine_rule->is_percent)) {
1257 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1258 $max_amount = $price * $max_fine_rule->amount / 100;
1260 $U->ou_ancestor_setting_value(
1262 'circ.max_fine.cap_at_price',
1266 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1267 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1275 sub run_copy_permit_scripts {
1277 my $copy = $self->copy || return;
1278 my $runner = $self->script_runner;
1282 if(!$self->legacy_script_support) {
1283 my $results = $self->run_indb_circ_test;
1284 push @allevents, $self->matrix_test_result_events
1285 unless $self->circ_test_success;
1288 # ---------------------------------------------------------------------
1289 # Capture all of the copy permit events
1290 # ---------------------------------------------------------------------
1291 $runner->load($self->circ_permit_copy);
1292 my $result = $runner->run or
1293 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1294 my $copy_events = $result->{events};
1296 # ---------------------------------------------------------------------
1297 # Now collect all of the events together
1298 # ---------------------------------------------------------------------
1299 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1302 # See if this copy has an alert message
1303 my $ae = $self->check_copy_alert();
1304 push( @allevents, $ae ) if $ae;
1306 # uniquify the events
1307 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1308 @allevents = values %hash;
1310 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1312 $self->push_events(@allevents);
1316 sub check_copy_alert {
1318 return undef if $self->is_renewal;
1319 return OpenILS::Event->new(
1320 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1321 if $self->copy and $self->copy->alert_message;
1327 # --------------------------------------------------------------------------
1328 # If the call is overriding and has permissions to override every collected
1329 # event, the are cleared. Any event that the caller does not have
1330 # permission to override, will be left in the event list and bail_out will
1332 # XXX We need code in here to cancel any holds/transits on copies
1333 # that are being force-checked out
1334 # --------------------------------------------------------------------------
1335 sub override_events {
1337 my @events = @{$self->events};
1338 return unless @events;
1340 if(!$self->override) {
1341 return $self->bail_out(1)
1342 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1347 for my $e (@events) {
1348 my $tc = $e->{textcode};
1349 next if $tc eq 'SUCCESS';
1350 my $ov = "$tc.override";
1351 $logger->info("circulator: attempting to override event: $ov");
1353 return $self->bail_on_events($self->editor->event)
1354 unless( $self->editor->allowed($ov) );
1359 # --------------------------------------------------------------------------
1360 # If there is an open claimsreturn circ on the requested copy, close the
1361 # circ if overriding, otherwise bail out
1362 # --------------------------------------------------------------------------
1363 sub handle_claims_returned {
1365 my $copy = $self->copy;
1367 my $CR = $self->editor->search_action_circulation(
1369 target_copy => $copy->id,
1370 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1371 checkin_time => undef,
1375 return unless ($CR = $CR->[0]);
1379 # - If the caller has set the override flag, we will check the item in
1380 if($self->override) {
1382 $CR->checkin_time('now');
1383 $CR->checkin_scan_time('now');
1384 $CR->checkin_lib($self->circ_lib);
1385 $CR->checkin_workstation($self->editor->requestor->wsid);
1386 $CR->checkin_staff($self->editor->requestor->id);
1388 $evt = $self->editor->event
1389 unless $self->editor->update_action_circulation($CR);
1392 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1395 $self->bail_on_events($evt) if $evt;
1400 # --------------------------------------------------------------------------
1401 # This performs the checkout
1402 # --------------------------------------------------------------------------
1406 $self->log_me("do_checkout()");
1408 # make sure perms are good if this isn't a renewal
1409 unless( $self->is_renewal ) {
1410 return $self->bail_on_events($self->editor->event)
1411 unless( $self->editor->allowed('COPY_CHECKOUT') );
1414 # verify the permit key
1415 unless( $self->check_permit_key ) {
1416 if( $self->permit_override ) {
1417 return $self->bail_on_events($self->editor->event)
1418 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1420 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1424 # if this is a non-cataloged circ, build the circ and finish
1425 if( $self->is_noncat ) {
1426 $self->checkout_noncat;
1428 OpenILS::Event->new('SUCCESS',
1429 payload => { noncat_circ => $self->circ }));
1433 if( $self->is_precat ) {
1434 $self->make_precat_copy;
1435 return if $self->bail_out;
1437 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1438 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1441 $self->do_copy_checks;
1442 return if $self->bail_out;
1444 $self->run_checkout_scripts();
1445 return if $self->bail_out;
1447 $self->build_checkout_circ_object();
1448 return if $self->bail_out;
1450 my $modify_to_start = $self->booking_adjusted_due_date();
1451 return if $self->bail_out;
1453 $self->apply_modified_due_date($modify_to_start);
1454 return if $self->bail_out;
1456 return $self->bail_on_events($self->editor->event)
1457 unless $self->editor->create_action_circulation($self->circ);
1459 # refresh the circ to force local time zone for now
1460 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1462 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1464 return if $self->bail_out;
1466 $self->apply_deposit_fee();
1467 return if $self->bail_out;
1469 $self->handle_checkout_holds();
1470 return if $self->bail_out;
1472 # ------------------------------------------------------------------------------
1473 # Update the patron penalty info in the DB. Run it for permit-overrides
1474 # since the penalties are not updated during the permit phase
1475 # ------------------------------------------------------------------------------
1476 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1478 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1481 if($self->is_renewal) {
1482 # flesh the billing summary for the checked-in circ
1483 $pcirc = $self->editor->retrieve_action_circulation([
1485 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1490 OpenILS::Event->new('SUCCESS',
1492 copy => $U->unflesh_copy($self->copy),
1493 circ => $self->circ,
1495 holds_fulfilled => $self->fulfilled_holds,
1496 deposit_billing => $self->deposit_billing,
1497 rental_billing => $self->rental_billing,
1498 parent_circ => $pcirc,
1499 patron => ($self->return_patron) ? $self->patron : undef,
1500 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1506 sub apply_deposit_fee {
1508 my $copy = $self->copy;
1510 ($self->is_deposit and not $self->is_deposit_exempt) or
1511 ($self->is_rental and not $self->is_rental_exempt);
1513 return if $self->is_deposit and $self->skip_deposit_fee;
1514 return if $self->is_rental and $self->skip_rental_fee;
1516 my $bill = Fieldmapper::money::billing->new;
1517 my $amount = $copy->deposit_amount;
1521 if($self->is_deposit) {
1522 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1524 $self->deposit_billing($bill);
1526 $billing_type = OILS_BILLING_TYPE_RENTAL;
1528 $self->rental_billing($bill);
1531 $bill->xact($self->circ->id);
1532 $bill->amount($amount);
1533 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1534 $bill->billing_type($billing_type);
1535 $bill->btype($btype);
1536 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1538 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1543 my $copy = $self->copy;
1545 my $stat = $copy->status if ref $copy->status;
1546 my $loc = $copy->location if ref $copy->location;
1547 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1549 $copy->status($stat->id) if $stat;
1550 $copy->location($loc->id) if $loc;
1551 $copy->circ_lib($circ_lib->id) if $circ_lib;
1552 $copy->editor($self->editor->requestor->id);
1553 $copy->edit_date('now');
1554 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1556 return $self->bail_on_events($self->editor->event)
1557 unless $self->editor->update_asset_copy($self->copy);
1559 $copy->status($U->copy_status($copy->status));
1560 $copy->location($loc) if $loc;
1561 $copy->circ_lib($circ_lib) if $circ_lib;
1564 sub update_reservation {
1566 my $reservation = $self->reservation;
1568 my $usr = $reservation->usr;
1569 my $target_rt = $reservation->target_resource_type;
1570 my $target_r = $reservation->target_resource;
1571 my $current_r = $reservation->current_resource;
1573 $reservation->usr($usr->id) if ref $usr;
1574 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1575 $reservation->target_resource($target_r->id) if ref $target_r;
1576 $reservation->current_resource($current_r->id) if ref $current_r;
1578 return $self->bail_on_events($self->editor->event)
1579 unless $self->editor->update_booking_reservation($self->reservation);
1582 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1583 $self->reservation($reservation);
1587 sub bail_on_events {
1588 my( $self, @evts ) = @_;
1589 $self->push_events(@evts);
1594 # ------------------------------------------------------------------------------
1595 # When an item is checked out, see if we can fulfill a hold for this patron
1596 # ------------------------------------------------------------------------------
1597 sub handle_checkout_holds {
1599 my $copy = $self->copy;
1600 my $patron = $self->patron;
1602 my $e = $self->editor;
1603 $self->fulfilled_holds([]);
1605 # pre/non-cats can't fulfill a hold
1606 return if $self->is_precat or $self->is_noncat;
1608 my $hold = $e->search_action_hold_request({
1609 current_copy => $copy->id ,
1610 cancel_time => undef,
1611 fulfillment_time => undef,
1613 {expire_time => undef},
1614 {expire_time => {'>' => 'now'}}
1618 if($hold and $hold->usr != $patron->id) {
1619 # reset the hold since the copy is now checked out
1621 $logger->info("circulator: un-targeting hold ".$hold->id.
1622 " because copy ".$copy->id." is getting checked out");
1624 $hold->clear_prev_check_time;
1625 $hold->clear_current_copy;
1626 $hold->clear_capture_time;
1628 return $self->bail_on_event($e->event)
1629 unless $e->update_action_hold_request($hold);
1635 $hold = $self->find_related_user_hold($copy, $patron) or return;
1636 $logger->info("circulator: found related hold to fulfill in checkout");
1639 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1641 # if the hold was never officially captured, capture it.
1642 $hold->current_copy($copy->id);
1643 $hold->capture_time('now') unless $hold->capture_time;
1644 $hold->fulfillment_time('now');
1645 $hold->fulfillment_staff($e->requestor->id);
1646 $hold->fulfillment_lib($self->circ_lib);
1648 return $self->bail_on_events($e->event)
1649 unless $e->update_action_hold_request($hold);
1651 $holdcode->delete_hold_copy_maps($e, $hold->id);
1652 return $self->fulfilled_holds([$hold->id]);
1656 # ------------------------------------------------------------------------------
1657 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1658 # the patron directly targets the checked out item, see if there is another hold
1659 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1660 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1661 # ------------------------------------------------------------------------------
1662 sub find_related_user_hold {
1663 my($self, $copy, $patron) = @_;
1664 my $e = $self->editor;
1666 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1668 return undef unless $U->ou_ancestor_setting_value(
1669 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1671 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1673 select => {ahr => ['id']},
1678 fkey => 'current_copy',
1679 type => 'left' # there may be no current_copy
1686 fulfillment_time => undef,
1687 cancel_time => undef,
1689 {expire_time => undef},
1690 {expire_time => {'>' => 'now'}}
1697 target => $self->volume->id
1703 target => $self->title->id
1709 {id => undef}, # left-join copy may be nonexistent
1710 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1714 order_by => {ahr => {request_time => {direction => 'asc'}}},
1718 my $hold_info = $e->json_query($args)->[0];
1719 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1724 sub run_checkout_scripts {
1729 my $runner = $self->script_runner;
1738 my $hard_due_date_name;
1740 if(!$self->legacy_script_support) {
1741 $self->run_indb_circ_test();
1742 $duration = $self->circ_matrix_matchpoint->duration_rule;
1743 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1744 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1745 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1749 $runner->load($self->circ_duration);
1751 my $result = $runner->run or
1752 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1754 $duration_name = $result->{durationRule};
1755 $recurring_name = $result->{recurringFinesRule};
1756 $max_fine_name = $result->{maxFine};
1757 $hard_due_date_name = $result->{hardDueDate};
1760 $duration_name = $duration->name if $duration;
1761 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1764 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1765 return $self->bail_on_events($evt) if ($evt && !$nobail);
1767 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1768 return $self->bail_on_events($evt) if ($evt && !$nobail);
1770 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1771 return $self->bail_on_events($evt) if ($evt && !$nobail);
1773 if($hard_due_date_name) {
1774 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1775 return $self->bail_on_events($evt) if ($evt && !$nobail);
1781 # The item circulates with an unlimited duration
1785 $hard_due_date = undef;
1788 $self->duration_rule($duration);
1789 $self->recurring_fines_rule($recurring);
1790 $self->max_fine_rule($max_fine);
1791 $self->hard_due_date($hard_due_date);
1795 sub build_checkout_circ_object {
1798 my $circ = Fieldmapper::action::circulation->new;
1799 my $duration = $self->duration_rule;
1800 my $max = $self->max_fine_rule;
1801 my $recurring = $self->recurring_fines_rule;
1802 my $hard_due_date = $self->hard_due_date;
1803 my $copy = $self->copy;
1804 my $patron = $self->patron;
1805 my $duration_date_ceiling;
1806 my $duration_date_ceiling_force;
1810 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1811 $duration_date_ceiling = $policy->{duration_date_ceiling};
1812 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1814 my $dname = $duration->name;
1815 my $mname = $max->name;
1816 my $rname = $recurring->name;
1818 if($hard_due_date) {
1819 $hdname = $hard_due_date->name;
1822 $logger->debug("circulator: building circulation ".
1823 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1825 $circ->duration($policy->{duration});
1826 $circ->recurring_fine($policy->{recurring_fine});
1827 $circ->duration_rule($duration->name);
1828 $circ->recurring_fine_rule($recurring->name);
1829 $circ->max_fine_rule($max->name);
1830 $circ->max_fine($policy->{max_fine});
1831 $circ->fine_interval($recurring->recurrence_interval);
1832 $circ->renewal_remaining($duration->max_renewals);
1836 $logger->info("circulator: copy found with an unlimited circ duration");
1837 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1838 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1839 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1840 $circ->renewal_remaining(0);
1843 $circ->target_copy( $copy->id );
1844 $circ->usr( $patron->id );
1845 $circ->circ_lib( $self->circ_lib );
1846 $circ->workstation($self->editor->requestor->wsid)
1847 if defined $self->editor->requestor->wsid;
1849 # renewals maintain a link to the parent circulation
1850 $circ->parent_circ($self->parent_circ);
1852 if( $self->is_renewal ) {
1853 $circ->opac_renewal('t') if $self->opac_renewal;
1854 $circ->phone_renewal('t') if $self->phone_renewal;
1855 $circ->desk_renewal('t') if $self->desk_renewal;
1856 $circ->renewal_remaining($self->renewal_remaining);
1857 $circ->circ_staff($self->editor->requestor->id);
1861 # if the user provided an overiding checkout time,
1862 # (e.g. the checkout really happened several hours ago), then
1863 # we apply that here. Does this need a perm??
1864 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1865 if $self->checkout_time;
1867 # if a patron is renewing, 'requestor' will be the patron
1868 $circ->circ_staff($self->editor->requestor->id);
1869 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1874 sub do_reservation_pickup {
1877 $self->log_me("do_reservation_pickup()");
1879 $self->reservation->pickup_time('now');
1882 $self->reservation->current_resource &&
1883 $U->is_true($self->reservation->target_resource_type->catalog_item)
1885 # We used to try to set $self->copy and $self->patron here,
1886 # but that should already be done.
1888 $self->run_checkout_scripts(1);
1890 my $duration = $self->duration_rule;
1891 my $max = $self->max_fine_rule;
1892 my $recurring = $self->recurring_fines_rule;
1894 if ($duration && $max && $recurring) {
1895 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1897 my $dname = $duration->name;
1898 my $mname = $max->name;
1899 my $rname = $recurring->name;
1901 $logger->debug("circulator: updating reservation ".
1902 "with duration=$dname, maxfine=$mname, recurring=$rname");
1904 $self->reservation->fine_amount($policy->{recurring_fine});
1905 $self->reservation->max_fine($policy->{max_fine});
1906 $self->reservation->fine_interval($recurring->recurrence_interval);
1909 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1910 $self->update_copy();
1913 $self->reservation->fine_amount(
1914 $self->reservation->target_resource_type->fine_amount
1916 $self->reservation->max_fine(
1917 $self->reservation->target_resource_type->max_fine
1919 $self->reservation->fine_interval(
1920 $self->reservation->target_resource_type->fine_interval
1924 $self->update_reservation();
1927 sub do_reservation_return {
1929 my $request = shift;
1931 $self->log_me("do_reservation_return()");
1933 if (not ref $self->reservation) {
1934 my ($reservation, $evt) =
1935 $U->fetch_booking_reservation($self->reservation);
1936 return $self->bail_on_events($evt) if $evt;
1937 $self->reservation($reservation);
1940 $self->generate_fines(1);
1941 $self->reservation->return_time('now');
1942 $self->update_reservation();
1943 $self->reshelve_copy if $self->copy;
1945 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1946 $self->copy( $self->reservation->current_resource->catalog_item );
1950 sub booking_adjusted_due_date {
1952 my $circ = $self->circ;
1953 my $copy = $self->copy;
1955 return undef unless $self->use_booking;
1959 if( $self->due_date ) {
1961 return $self->bail_on_events($self->editor->event)
1962 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1964 $circ->due_date(cleanse_ISO8601($self->due_date));
1968 return unless $copy and $circ->due_date;
1971 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1972 if (@$booking_items) {
1973 my $booking_item = $booking_items->[0];
1974 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1976 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1977 my $shorten_circ_setting = $resource_type->elbow_room ||
1978 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1981 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1982 my $bookings = $booking_ses->request(
1983 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1984 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1986 $booking_ses->disconnect;
1988 my $dt_parser = DateTime::Format::ISO8601->new;
1989 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1991 for my $bid (@$bookings) {
1993 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1995 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1996 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1998 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1999 if ($booking_start < DateTime->now);
2002 if ($U->is_true($stop_circ_setting)) {
2003 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2005 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2006 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2009 # We set the circ duration here only to affect the logic that will
2010 # later (in a DB trigger) mangle the time part of the due date to
2011 # 11:59pm. Having any circ duration that is not a whole number of
2012 # days is enough to prevent the "correction."
2013 my $new_circ_duration = $due_date->epoch - time;
2014 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2015 $circ->duration("$new_circ_duration seconds");
2017 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2021 return $self->bail_on_events($self->editor->event)
2022 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2028 sub apply_modified_due_date {
2030 my $shift_earlier = shift;
2031 my $circ = $self->circ;
2032 my $copy = $self->copy;
2034 if( $self->due_date ) {
2036 return $self->bail_on_events($self->editor->event)
2037 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2039 $circ->due_date(cleanse_ISO8601($self->due_date));
2043 # if the due_date lands on a day when the location is closed
2044 return unless $copy and $circ->due_date;
2046 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2048 # due-date overlap should be determined by the location the item
2049 # is checked out from, not the owning or circ lib of the item
2050 my $org = $self->circ_lib;
2052 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2053 " with an item due date of ".$circ->due_date );
2055 my $dateinfo = $U->storagereq(
2056 'open-ils.storage.actor.org_unit.closed_date.overlap',
2057 $org, $circ->due_date );
2060 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2061 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2063 # XXX make the behavior more dynamic
2064 # for now, we just push the due date to after the close date
2065 if ($shift_earlier) {
2066 $circ->due_date($dateinfo->{start});
2068 $circ->due_date($dateinfo->{end});
2076 sub create_due_date {
2077 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2079 # if there is a raw time component (e.g. from postgres),
2080 # turn it into an interval that interval_to_seconds can parse
2081 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2083 # for now, use the server timezone. TODO: use workstation org timezone
2084 my $due_date = DateTime->now(time_zone => 'local');
2086 # add the circ duration
2087 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2090 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2091 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2092 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2097 # return ISO8601 time with timezone
2098 return $due_date->strftime('%FT%T%z');
2103 sub make_precat_copy {
2105 my $copy = $self->copy;
2108 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2110 $copy->editor($self->editor->requestor->id);
2111 $copy->edit_date('now');
2112 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2113 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2114 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2115 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2116 $self->update_copy();
2120 $logger->info("circulator: Creating a new precataloged ".
2121 "copy in checkout with barcode " . $self->copy_barcode);
2123 $copy = Fieldmapper::asset::copy->new;
2124 $copy->circ_lib($self->circ_lib);
2125 $copy->creator($self->editor->requestor->id);
2126 $copy->editor($self->editor->requestor->id);
2127 $copy->barcode($self->copy_barcode);
2128 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2129 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2130 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2132 $copy->dummy_title($self->dummy_title || "");
2133 $copy->dummy_author($self->dummy_author || "");
2134 $copy->dummy_isbn($self->dummy_isbn || "");
2135 $copy->circ_modifier($self->circ_modifier);
2138 # See if we need to override the circ_lib for the copy with a configured circ_lib
2139 # Setting is shortname of the org unit
2140 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2141 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2143 if($precat_circ_lib) {
2144 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2147 $self->bail_on_events($self->editor->event);
2151 $copy->circ_lib($org->id);
2155 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2157 $self->push_events($self->editor->event);
2161 # this is a little bit of a hack, but we need to
2162 # get the copy into the script runner
2163 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2167 sub checkout_noncat {
2173 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2174 my $count = $self->noncat_count || 1;
2175 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2177 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2181 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2182 $self->editor->requestor->id,
2190 $self->push_events($evt);
2201 $self->log_me("do_checkin()");
2203 return $self->bail_on_events(
2204 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2207 # the renew code and mk_env should have already found our circulation object
2208 unless( $self->circ ) {
2210 my $circs = $self->editor->search_action_circulation(
2211 { target_copy => $self->copy->id, checkin_time => undef });
2213 $self->circ($$circs[0]);
2215 # for now, just warn if there are multiple open circs on a copy
2216 $logger->warn("circulator: we have ".scalar(@$circs).
2217 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2220 # run the fine generator against this circ, if this circ is there
2221 $self->generate_fines_start if $self->circ;
2224 if( $self->checkin_check_holds_shelf() ) {
2225 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2226 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2227 $self->checkin_flesh_events;
2231 unless( $self->is_renewal ) {
2232 return $self->bail_on_events($self->editor->event)
2233 unless $self->editor->allowed('COPY_CHECKIN');
2236 $self->push_events($self->check_copy_alert());
2237 $self->push_events($self->check_checkin_copy_status());
2239 # if the circ is marked as 'claims returned', add the event to the list
2240 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2241 if ($self->circ and $self->circ->stop_fines
2242 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2244 $self->check_circ_deposit();
2246 # handle the overridable events
2247 $self->override_events unless $self->is_renewal;
2248 return if $self->bail_out;
2252 $self->editor->search_action_transit_copy(
2253 { target_copy => $self->copy->id, dest_recv_time => undef }
2259 $self->checkin_handle_circ;
2260 return if $self->bail_out;
2261 $self->checkin_changed(1);
2263 } elsif( $self->transit ) {
2264 my $hold_transit = $self->process_received_transit;
2265 $self->checkin_changed(1);
2267 if( $self->bail_out ) {
2268 $self->checkin_flesh_events;
2272 if( my $e = $self->check_checkin_copy_status() ) {
2273 # If the original copy status is special, alert the caller
2274 my $ev = $self->events;
2275 $self->events([$e]);
2276 $self->override_events;
2277 return if $self->bail_out;
2281 if( $hold_transit or
2282 $U->copy_status($self->copy->status)->id
2283 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2286 if( $hold_transit ) {
2287 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2289 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2294 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2296 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2297 $self->reshelve_copy(1);
2298 $self->cancelled_hold_transit(1);
2299 $self->notify_hold(0); # don't notify for cancelled holds
2300 return if $self->bail_out;
2304 # hold transited to correct location
2305 $self->checkin_flesh_events;
2310 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2312 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2313 " that is in-transit, but there is no transit.. repairing");
2314 $self->reshelve_copy(1);
2315 return if $self->bail_out;
2318 if( $self->is_renewal ) {
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 # gather any updates to the circ after fine generation, if there was a circ
2429 $self->generate_fines_finish if ($self->circ);
2431 OpenILS::Utils::Penalty->calculate_penalties(
2432 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2434 $self->checkin_flesh_events;
2438 # if a deposit was payed for this item, push the event
2439 sub check_circ_deposit {
2441 return unless $self->circ;
2442 my $deposit = $self->editor->search_money_billing(
2444 xact => $self->circ->id,
2446 }, {idlist => 1})->[0];
2448 $self->push_events(OpenILS::Event->new(
2449 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2454 my $force = $self->force || shift;
2455 my $copy = $self->copy;
2457 my $stat = $U->copy_status($copy->status)->id;
2460 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2461 $stat != OILS_COPY_STATUS_CATALOGING and
2462 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2463 $stat != OILS_COPY_STATUS_RESHELVING )) {
2465 $copy->status( OILS_COPY_STATUS_RESHELVING );
2467 $self->checkin_changed(1);
2472 # Returns true if the item is at the current location
2473 # because it was transited there for a hold and the
2474 # hold has not been fulfilled
2475 sub checkin_check_holds_shelf {
2477 return 0 unless $self->copy;
2480 $U->copy_status($self->copy->status)->id ==
2481 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2483 # find the hold that put us on the holds shelf
2484 my $holds = $self->editor->search_action_hold_request(
2486 current_copy => $self->copy->id,
2487 capture_time => { '!=' => undef },
2488 fulfillment_time => undef,
2489 cancel_time => undef,
2494 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2495 $self->reshelve_copy(1);
2499 my $hold = $$holds[0];
2501 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2502 $hold->id. "] for copy ".$self->copy->barcode);
2504 if( $hold->pickup_lib == $self->circ_lib ) {
2505 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2509 $logger->info("circulator: hold is not for here..");
2510 $self->remote_hold($hold);
2515 sub checkin_handle_precat {
2517 my $copy = $self->copy;
2519 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2520 $copy->status(OILS_COPY_STATUS_CATALOGING);
2521 $self->update_copy();
2522 $self->checkin_changed(1);
2523 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2528 sub checkin_build_copy_transit {
2531 my $copy = $self->copy;
2532 my $transit = Fieldmapper::action::transit_copy->new;
2534 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2535 $logger->info("circulator: transiting copy to $dest");
2537 $transit->source($self->circ_lib);
2538 $transit->dest($dest);
2539 $transit->target_copy($copy->id);
2540 $transit->source_send_time('now');
2541 $transit->copy_status( $U->copy_status($copy->status)->id );
2543 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2545 return $self->bail_on_events($self->editor->event)
2546 unless $self->editor->create_action_transit_copy($transit);
2548 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2550 $self->checkin_changed(1);
2554 sub hold_capture_is_possible {
2556 my $copy = $self->copy;
2558 # we've been explicitly told not to capture any holds
2559 return 0 if $self->capture eq 'nocapture';
2561 # See if this copy can fulfill any holds
2562 my $hold = $holdcode->find_nearest_permitted_hold(
2563 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2565 return undef if ref $hold eq "HASH" and
2566 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2570 sub reservation_capture_is_possible {
2572 my $copy = $self->copy;
2574 # we've been explicitly told not to capture any holds
2575 return 0 if $self->capture eq 'nocapture';
2577 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2578 my $resv = $booking_ses->request(
2579 "open-ils.booking.reservations.could_capture",
2580 $self->editor->authtoken, $copy->barcode
2582 $booking_ses->disconnect;
2583 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2584 $self->push_events($resv);
2590 # returns true if the item was used (or may potentially be used
2591 # in subsequent calls) to capture a hold.
2592 sub attempt_checkin_hold_capture {
2594 my $copy = $self->copy;
2596 # we've been explicitly told not to capture any holds
2597 return 0 if $self->capture eq 'nocapture';
2599 # See if this copy can fulfill any holds
2600 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2601 $self->editor, $copy, $self->editor->requestor );
2604 $logger->debug("circulator: no potential permitted".
2605 "holds found for copy ".$copy->barcode);
2609 if($self->capture ne 'capture') {
2610 # see if this item is in a hold-capture-delay location
2611 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2612 if($U->is_true($location->hold_verify)) {
2613 $self->bail_on_events(
2614 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2619 $self->retarget($retarget);
2621 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2623 $hold->current_copy($copy->id);
2624 $hold->capture_time('now');
2625 $self->put_hold_on_shelf($hold)
2626 if $hold->pickup_lib == $self->circ_lib;
2628 # prevent DB errors caused by fetching
2629 # holds from storage, and updating through cstore
2630 $hold->clear_fulfillment_time;
2631 $hold->clear_fulfillment_staff;
2632 $hold->clear_fulfillment_lib;
2633 $hold->clear_expire_time;
2634 $hold->clear_cancel_time;
2635 $hold->clear_prev_check_time unless $hold->prev_check_time;
2637 $self->bail_on_events($self->editor->event)
2638 unless $self->editor->update_action_hold_request($hold);
2640 $self->checkin_changed(1);
2642 return 0 if $self->bail_out;
2644 if( $hold->pickup_lib == $self->circ_lib ) {
2646 # This hold was captured in the correct location
2647 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2648 $self->push_events(OpenILS::Event->new('SUCCESS'));
2650 #$self->do_hold_notify($hold->id);
2651 $self->notify_hold($hold->id);
2655 # Hold needs to be picked up elsewhere. Build a hold
2656 # transit and route the item.
2657 $self->checkin_build_hold_transit();
2658 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2659 return 0 if $self->bail_out;
2660 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2663 # make sure we save the copy status
2668 sub attempt_checkin_reservation_capture {
2670 my $copy = $self->copy;
2672 # we've been explicitly told not to capture any holds
2673 return 0 if $self->capture eq 'nocapture';
2675 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2676 my $evt = $booking_ses->request(
2677 "open-ils.booking.resources.capture_for_reservation",
2678 $self->editor->authtoken,
2680 1 # don't update copy - we probably have it locked
2682 $booking_ses->disconnect;
2684 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2686 "open-ils.booking.resources.capture_for_reservation " .
2687 "didn't return an event!"
2691 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2692 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2694 # not-transferable is an error event we'll pass on the user
2695 $logger->warn("reservation capture attempted against non-transferable item");
2696 $self->push_events($evt);
2698 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2699 # Re-retrieve copy as reservation capture may have changed
2700 # its status and whatnot.
2702 "circulator: booking capture win on copy " . $self->copy->id
2704 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2706 "circulator: changing copy " . $self->copy->id .
2707 "'s status from " . $self->copy->status . " to " .
2710 $self->copy->status($new_copy_status);
2713 $self->reservation($evt->{"payload"}->{"reservation"});
2715 if (exists $evt->{"payload"}->{"transit"}) {
2719 "org" => $evt->{"payload"}->{"transit"}->dest
2723 $self->checkin_changed(1);
2727 # other results are treated as "nothing to capture"
2731 sub do_hold_notify {
2732 my( $self, $holdid ) = @_;
2734 my $e = new_editor(xact => 1);
2735 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2737 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2738 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2740 $logger->info("circulator: running delayed hold notify process");
2742 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2743 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2745 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2746 hold_id => $holdid, requestor => $self->editor->requestor);
2748 $logger->debug("circulator: built hold notifier");
2750 if(!$notifier->event) {
2752 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2754 my $stat = $notifier->send_email_notify;
2755 if( $stat == '1' ) {
2756 $logger->info("circulator: hold notify succeeded for hold $holdid");
2760 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2763 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2767 sub retarget_holds {
2769 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2770 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2771 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2772 # no reason to wait for the return value
2776 sub checkin_build_hold_transit {
2779 my $copy = $self->copy;
2780 my $hold = $self->hold;
2781 my $trans = Fieldmapper::action::hold_transit_copy->new;
2783 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2785 $trans->hold($hold->id);
2786 $trans->source($self->circ_lib);
2787 $trans->dest($hold->pickup_lib);
2788 $trans->source_send_time("now");
2789 $trans->target_copy($copy->id);
2791 # when the copy gets to its destination, it will recover
2792 # this status - put it onto the holds shelf
2793 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2795 return $self->bail_on_events($self->editor->event)
2796 unless $self->editor->create_action_hold_transit_copy($trans);
2801 sub process_received_transit {
2803 my $copy = $self->copy;
2804 my $copyid = $self->copy->id;
2806 my $status_name = $U->copy_status($copy->status)->name;
2807 $logger->debug("circulator: attempting transit receive on ".
2808 "copy $copyid. Copy status is $status_name");
2810 my $transit = $self->transit;
2812 if( $transit->dest != $self->circ_lib ) {
2813 # - this item is in-transit to a different location
2815 my $tid = $transit->id;
2816 my $loc = $self->circ_lib;
2817 my $dest = $transit->dest;
2819 $logger->info("circulator: Fowarding transit on copy which is destined ".
2820 "for a different location. transit=$tid, copy=$copyid, current ".
2821 "location=$loc, destination location=$dest");
2823 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2825 # grab the associated hold object if available
2826 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2827 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2829 return $self->bail_on_events($evt);
2832 # The transit is received, set the receive time
2833 $transit->dest_recv_time('now');
2834 $self->bail_on_events($self->editor->event)
2835 unless $self->editor->update_action_transit_copy($transit);
2837 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2839 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2840 $copy->status( $transit->copy_status );
2841 $self->update_copy();
2842 return if $self->bail_out;
2846 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2848 # hold has arrived at destination, set shelf time
2849 $self->put_hold_on_shelf($hold);
2850 $self->bail_on_events($self->editor->event)
2851 unless $self->editor->update_action_hold_request($hold);
2852 return if $self->bail_out;
2854 $self->notify_hold($hold_transit->hold);
2859 OpenILS::Event->new(
2862 payload => { transit => $transit, holdtransit => $hold_transit } ));
2864 return $hold_transit;
2868 # ------------------------------------------------------------------
2869 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2870 # ------------------------------------------------------------------
2871 sub put_hold_on_shelf {
2872 my($self, $hold) = @_;
2874 $hold->shelf_time('now');
2876 my $shelf_expire = $U->ou_ancestor_setting_value(
2877 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2880 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2881 my $expire_time = DateTime->now->add(seconds => $seconds);
2882 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2890 sub generate_fines {
2892 my $reservation = shift;
2894 $self->generate_fines_start($reservation);
2895 $self->generate_fines_finish($reservation);
2900 sub generate_fines_start {
2902 my $reservation = shift;
2904 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2906 if (!exists($self->{_gen_fines_req})) {
2907 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
2909 'open-ils.storage.action.circulation.overdue.generate_fines',
2918 sub generate_fines_finish {
2920 my $reservation = shift;
2922 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2924 $self->{_gen_fines_req}->wait_complete;
2925 delete($self->{_gen_fines_req});
2927 # refresh the circ in case the fine generator set the stop_fines field
2928 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2929 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2934 sub checkin_handle_circ {
2936 my $circ = $self->circ;
2937 my $copy = $self->copy;
2941 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2943 # backdate the circ if necessary
2944 if($self->backdate) {
2945 my $evt = $self->checkin_handle_backdate;
2946 return $self->bail_on_events($evt) if $evt;
2949 if($self->void_overdues) {
2950 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2951 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2952 return $self->bail_on_events($evt) if $evt;
2955 if(!$circ->stop_fines) {
2956 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2957 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2958 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2959 $circ->stop_fines_time('now');
2960 $circ->stop_fines_time($self->backdate) if $self->backdate;
2963 # Set the checkin vars since we have the item
2964 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2966 # capture the true scan time for back-dated checkins
2967 $circ->checkin_scan_time('now');
2969 $circ->checkin_staff($self->editor->requestor->id);
2970 $circ->checkin_lib($self->circ_lib);
2971 $circ->checkin_workstation($self->editor->requestor->wsid);
2973 my $circ_lib = (ref $self->copy->circ_lib) ?
2974 $self->copy->circ_lib->id : $self->copy->circ_lib;
2975 my $stat = $U->copy_status($self->copy->status)->id;
2977 # immediately available keeps items lost or missing items from going home before being handled
2978 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2979 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2982 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
2984 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2985 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2987 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2991 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2993 $self->checkin_handle_lost($circ_lib);
2997 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3002 # see if there are any fines owed on this circ. if not, close it
3003 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3004 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3006 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3008 return $self->bail_on_events($self->editor->event)
3009 unless $self->editor->update_action_circulation($circ);
3011 # make sure the circ isn't closed if we just voided some fines
3012 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
3013 return $self->bail_on_events($evt) if $evt;
3019 # ------------------------------------------------------------------
3020 # See if we need to void billings for lost checkin
3021 # ------------------------------------------------------------------
3022 sub checkin_handle_lost {
3024 my $circ_lib = shift;
3025 my $circ = $self->circ;
3027 my $max_return = $U->ou_ancestor_setting_value(
3028 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3033 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3034 $tm[5] -= 1 if $tm[5] > 0;
3035 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3037 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3038 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3040 $max_return = 0 if $today < $last_chance;
3043 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3045 my $void_lost = $U->ou_ancestor_setting_value(
3046 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3047 my $void_lost_fee = $U->ou_ancestor_setting_value(
3048 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3049 my $restore_od = $U->ou_ancestor_setting_value(
3050 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3052 $self->checkin_handle_lost_now_found(3) if $void_lost;
3053 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3054 $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3057 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3062 sub checkin_handle_backdate {
3065 # ------------------------------------------------------------------
3066 # clean up the backdate for date comparison
3067 # XXX We are currently taking the due-time from the original due-date,
3068 # not the input. Do we need to do this? This certainly interferes with
3069 # backdating of hourly checkouts, but that is likely a very rare case.
3070 # ------------------------------------------------------------------
3071 my $bd = cleanse_ISO8601($self->backdate);
3072 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3073 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3074 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3076 $self->backdate($bd);
3078 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($self->editor, $self->circ, $bd);
3079 return $evt if $evt;
3085 sub check_checkin_copy_status {
3087 my $copy = $self->copy;
3089 my $status = $U->copy_status($copy->status)->id;
3092 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3093 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3094 $status == OILS_COPY_STATUS_IN_PROCESS ||
3095 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3096 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3097 $status == OILS_COPY_STATUS_CATALOGING ||
3098 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3099 $status == OILS_COPY_STATUS_RESHELVING );
3101 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3102 if( $status == OILS_COPY_STATUS_LOST );
3104 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3105 if( $status == OILS_COPY_STATUS_MISSING );
3107 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3112 # --------------------------------------------------------------------------
3113 # On checkin, we need to return as many relevant objects as we can
3114 # --------------------------------------------------------------------------
3115 sub checkin_flesh_events {
3118 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3119 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3120 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3123 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3126 if($self->hold and !$self->hold->cancel_time) {
3127 $hold = $self->hold;
3128 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3132 # if we checked in a circulation, flesh the billing summary data
3133 $self->circ->billable_transaction(
3134 $self->editor->retrieve_money_billable_transaction([
3136 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3142 # flesh some patron fields before returning
3144 $self->editor->retrieve_actor_user([
3149 au => ['card', 'billing_address', 'mailing_address']
3156 for my $evt (@{$self->events}) {
3159 $payload->{copy} = $U->unflesh_copy($self->copy);
3160 $payload->{record} = $record,
3161 $payload->{circ} = $self->circ;
3162 $payload->{transit} = $self->transit;
3163 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3164 $payload->{hold} = $hold;
3165 $payload->{patron} = $self->patron;
3166 $payload->{reservation} = $self->reservation
3167 unless (not $self->reservation or $self->reservation->cancel_time);
3169 $evt->{payload} = $payload;
3174 my( $self, $msg ) = @_;
3175 my $bc = ($self->copy) ? $self->copy->barcode :
3178 my $usr = ($self->patron) ? $self->patron->id : "";
3179 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3180 ", recipient=$usr, copy=$bc");
3186 $self->log_me("do_renew()");
3188 # Make sure there is an open circ to renew that is not
3189 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3190 my $usrid = $self->patron->id if $self->patron;
3191 my $circ = $self->editor->search_action_circulation({
3192 target_copy => $self->copy->id,
3193 xact_finish => undef,
3194 ($usrid ? (usr => $usrid) : ()),
3196 {stop_fines => undef},
3197 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3201 return $self->bail_on_events($self->editor->event) unless $circ;
3203 # A user is not allowed to renew another user's items without permission
3204 unless( $circ->usr eq $self->editor->requestor->id ) {
3205 return $self->bail_on_events($self->editor->events)
3206 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3209 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3210 if $circ->renewal_remaining < 1;
3212 # -----------------------------------------------------------------
3214 $self->parent_circ($circ->id);
3215 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3218 # Run the fine generator against the old circ
3219 $self->generate_fines_start;
3221 $self->run_renew_permit;
3224 $self->do_checkin();
3225 return if $self->bail_out;
3227 unless( $self->permit_override ) {
3229 return if $self->bail_out;
3230 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3231 $self->remove_event('ITEM_NOT_CATALOGED');
3234 $self->override_events;
3235 return if $self->bail_out;
3238 $self->do_checkout();
3243 my( $self, $evt ) = @_;
3244 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3245 $logger->debug("circulator: removing event from list: $evt");
3246 my @events = @{$self->events};
3247 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3252 my( $self, $evt ) = @_;
3253 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3254 return grep { $_->{textcode} eq $evt } @{$self->events};
3259 sub run_renew_permit {
3262 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3263 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3264 $self->editor, $self->copy, $self->editor->requestor, 1
3266 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3269 if(!$self->legacy_script_support) {
3270 my $results = $self->run_indb_circ_test;
3271 $self->push_events($self->matrix_test_result_events)
3272 unless $self->circ_test_success;
3275 my $runner = $self->script_runner;
3277 $runner->load($self->circ_permit_renew);
3278 my $result = $runner->run or
3279 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3280 if ($result->{"events"}) {
3282 map { new OpenILS::Event($_) } @{$result->{"events"}}
3285 "circulator: circ_permit_renew for user " .
3286 $self->patron->id . " returned " .
3287 scalar(@{$result->{"events"}}) . " event(s)"
3291 $self->mk_script_runner;
3294 $logger->debug("circulator: re-creating script runner to be safe");
3298 # XXX: The primary mechanism for storing circ history is now handled
3299 # by tracking real circulation objects instead of bibs in a bucket.
3300 # However, this code is disabled by default and could be useful
3301 # some day, so may as well leave it for now.
3302 sub append_reading_list {
3306 $self->is_checkout and
3312 # verify history is globally enabled and uses the bucket mechanism
3313 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3314 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3316 return undef unless $htype and $htype eq 'bucket';
3318 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3320 # verify the patron wants to retain the hisory
3321 my $setting = $e->search_actor_user_setting(
3322 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3324 unless($setting and $setting->value) {
3329 my $bkt = $e->search_container_copy_bucket(
3330 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3335 # find the next item position
3336 my $last_item = $e->search_container_copy_bucket_item(
3337 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3338 $pos = $last_item->pos + 1 if $last_item;
3341 # create the history bucket if necessary
3342 $bkt = Fieldmapper::container::copy_bucket->new;
3343 $bkt->owner($self->patron->id);
3345 $bkt->btype('circ_history');
3347 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3350 my $item = Fieldmapper::container::copy_bucket_item->new;
3352 $item->bucket($bkt->id);
3353 $item->target_copy($self->copy->id);
3356 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3363 sub make_trigger_events {
3365 return unless $self->circ;
3366 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3367 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3368 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3373 sub checkin_handle_lost_now_found {
3374 my ($self, $bill_type) = @_;
3376 # ------------------------------------------------------------------
3377 # remove charge from patron's account if lost item is returned
3378 # ------------------------------------------------------------------
3380 my $bills = $self->editor->search_money_billing(
3382 xact => $self->circ->id,
3387 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3388 for my $bill (@$bills) {
3389 if( !$U->is_true($bill->voided) ) {
3390 $logger->info("lost item returned - voiding bill ".$bill->id);
3392 $bill->void_time('now');
3393 $bill->voider($self->editor->requestor->id);
3394 my $note = ($bill->note) ? $bill->note . "\n" : '';
3395 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3397 $self->bail_on_events($self->editor->event)
3398 unless $self->editor->update_money_billing($bill);
3403 sub checkin_handle_lost_now_found_restore_od {
3406 # ------------------------------------------------------------------
3407 # restore those overdue charges voided when item was set to lost
3408 # ------------------------------------------------------------------
3410 my $ods = $self->editor->search_money_billing(
3412 xact => $self->circ->id,
3417 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3418 for my $bill (@$ods) {
3419 if( $U->is_true($bill->voided) ) {
3420 $logger->info("lost item returned - restoring overdue ".$bill->id);
3422 $bill->clear_void_time;
3423 $bill->voider($self->editor->requestor->id);
3424 my $note = ($bill->note) ? $bill->note . "\n" : '';
3425 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3427 $self->bail_on_events($self->editor->event)
3428 unless $self->editor->update_money_billing($bill);