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;
2223 if( $self->checkin_check_holds_shelf() ) {
2224 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2225 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2226 $self->checkin_flesh_events;
2230 unless( $self->is_renewal ) {
2231 return $self->bail_on_events($self->editor->event)
2232 unless $self->editor->allowed('COPY_CHECKIN');
2235 $self->push_events($self->check_copy_alert());
2236 $self->push_events($self->check_checkin_copy_status());
2238 # if the circ is marked as 'claims returned', add the event to the list
2239 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2240 if ($self->circ and $self->circ->stop_fines
2241 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2243 $self->check_circ_deposit();
2245 # handle the overridable events
2246 $self->override_events unless $self->is_renewal;
2247 return if $self->bail_out;
2251 $self->editor->search_action_transit_copy(
2252 { target_copy => $self->copy->id, dest_recv_time => undef }
2258 $self->checkin_handle_circ;
2259 return if $self->bail_out;
2260 $self->checkin_changed(1);
2262 } elsif( $self->transit ) {
2263 my $hold_transit = $self->process_received_transit;
2264 $self->checkin_changed(1);
2266 if( $self->bail_out ) {
2267 $self->checkin_flesh_events;
2271 if( my $e = $self->check_checkin_copy_status() ) {
2272 # If the original copy status is special, alert the caller
2273 my $ev = $self->events;
2274 $self->events([$e]);
2275 $self->override_events;
2276 return if $self->bail_out;
2280 if( $hold_transit or
2281 $U->copy_status($self->copy->status)->id
2282 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2285 if( $hold_transit ) {
2286 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2288 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2293 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2295 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2296 $self->reshelve_copy(1);
2297 $self->cancelled_hold_transit(1);
2298 $self->notify_hold(0); # don't notify for cancelled holds
2299 return if $self->bail_out;
2303 # hold transited to correct location
2304 $self->checkin_flesh_events;
2309 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2311 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2312 " that is in-transit, but there is no transit.. repairing");
2313 $self->reshelve_copy(1);
2314 return if $self->bail_out;
2317 if( $self->is_renewal ) {
2318 $self->finish_fines_and_voiding;
2319 return if $self->bail_out;
2320 $self->push_events(OpenILS::Event->new('SUCCESS'));
2324 # ------------------------------------------------------------------------------
2325 # Circulations and transits are now closed where necessary. Now go on to see if
2326 # this copy can fulfill a hold or needs to be routed to a different location
2327 # ------------------------------------------------------------------------------
2329 my $needed_for_something = 0; # formerly "needed_for_hold"
2331 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2333 if (!$self->remote_hold) {
2334 if ($self->use_booking) {
2335 my $potential_hold = $self->hold_capture_is_possible;
2336 my $potential_reservation = $self->reservation_capture_is_possible;
2338 if ($potential_hold and $potential_reservation) {
2339 $logger->info("circulator: item could fulfill either hold or reservation");
2340 $self->push_events(new OpenILS::Event(
2341 "HOLD_RESERVATION_CONFLICT",
2342 "hold" => $potential_hold,
2343 "reservation" => $potential_reservation
2345 return if $self->bail_out;
2346 } elsif ($potential_hold) {
2347 $needed_for_something =
2348 $self->attempt_checkin_hold_capture;
2349 } elsif ($potential_reservation) {
2350 $needed_for_something =
2351 $self->attempt_checkin_reservation_capture;
2354 $needed_for_something = $self->attempt_checkin_hold_capture;
2357 return if $self->bail_out;
2359 unless($needed_for_something) {
2360 my $circ_lib = (ref $self->copy->circ_lib) ?
2361 $self->copy->circ_lib->id : $self->copy->circ_lib;
2363 if( $self->remote_hold ) {
2364 $circ_lib = $self->remote_hold->pickup_lib;
2365 $logger->warn("circulator: Copy ".$self->copy->barcode.
2366 " is on a remote hold's shelf, sending to $circ_lib");
2369 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2371 if( $circ_lib == $self->circ_lib) {
2372 # copy is where it needs to be, either for hold or reshelving
2374 $self->checkin_handle_precat();
2375 return if $self->bail_out;
2378 # copy needs to transit "home", or stick here if it's a floating copy
2380 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2381 $self->checkin_changed(1);
2382 $self->copy->circ_lib( $self->circ_lib );
2385 my $bc = $self->copy->barcode;
2386 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2387 $self->checkin_build_copy_transit($circ_lib);
2388 return if $self->bail_out;
2389 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2393 } else { # no-op checkin
2394 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2395 $self->checkin_changed(1);
2396 $self->copy->circ_lib( $self->circ_lib );
2401 if($self->claims_never_checked_out and
2402 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2404 # the item was not supposed to be checked out to the user and should now be marked as missing
2405 $self->copy->status(OILS_COPY_STATUS_MISSING);
2409 $self->reshelve_copy unless $needed_for_something;
2412 return if $self->bail_out;
2414 unless($self->checkin_changed) {
2416 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2417 my $stat = $U->copy_status($self->copy->status)->id;
2419 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2420 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2421 $self->bail_out(1); # no need to commit anything
2425 $self->push_events(OpenILS::Event->new('SUCCESS'))
2426 unless @{$self->events};
2429 $self->finish_fines_and_voiding;
2431 OpenILS::Utils::Penalty->calculate_penalties(
2432 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2434 $self->checkin_flesh_events;
2438 sub finish_fines_and_voiding {
2440 return unless $self->circ;
2442 # gather any updates to the circ after fine generation, if there was a circ
2443 $self->generate_fines_finish;
2445 return unless $self->backdate or $self->void_overdues;
2447 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2448 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2450 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2451 $self->editor, $self->circ, $self->backdate, $note);
2453 return $self->bail_on_events($evt) if $evt;
2455 # make sure the circ isn't closed if we just voided some fines
2456 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2457 return $self->bail_on_events($evt) if $evt;
2463 # if a deposit was payed for this item, push the event
2464 sub check_circ_deposit {
2466 return unless $self->circ;
2467 my $deposit = $self->editor->search_money_billing(
2469 xact => $self->circ->id,
2471 }, {idlist => 1})->[0];
2473 $self->push_events(OpenILS::Event->new(
2474 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2479 my $force = $self->force || shift;
2480 my $copy = $self->copy;
2482 my $stat = $U->copy_status($copy->status)->id;
2485 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2486 $stat != OILS_COPY_STATUS_CATALOGING and
2487 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2488 $stat != OILS_COPY_STATUS_RESHELVING )) {
2490 $copy->status( OILS_COPY_STATUS_RESHELVING );
2492 $self->checkin_changed(1);
2497 # Returns true if the item is at the current location
2498 # because it was transited there for a hold and the
2499 # hold has not been fulfilled
2500 sub checkin_check_holds_shelf {
2502 return 0 unless $self->copy;
2505 $U->copy_status($self->copy->status)->id ==
2506 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2508 # find the hold that put us on the holds shelf
2509 my $holds = $self->editor->search_action_hold_request(
2511 current_copy => $self->copy->id,
2512 capture_time => { '!=' => undef },
2513 fulfillment_time => undef,
2514 cancel_time => undef,
2519 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2520 $self->reshelve_copy(1);
2524 my $hold = $$holds[0];
2526 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2527 $hold->id. "] for copy ".$self->copy->barcode);
2529 if( $hold->pickup_lib == $self->circ_lib ) {
2530 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2534 $logger->info("circulator: hold is not for here..");
2535 $self->remote_hold($hold);
2540 sub checkin_handle_precat {
2542 my $copy = $self->copy;
2544 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2545 $copy->status(OILS_COPY_STATUS_CATALOGING);
2546 $self->update_copy();
2547 $self->checkin_changed(1);
2548 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2553 sub checkin_build_copy_transit {
2556 my $copy = $self->copy;
2557 my $transit = Fieldmapper::action::transit_copy->new;
2559 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2560 $logger->info("circulator: transiting copy to $dest");
2562 $transit->source($self->circ_lib);
2563 $transit->dest($dest);
2564 $transit->target_copy($copy->id);
2565 $transit->source_send_time('now');
2566 $transit->copy_status( $U->copy_status($copy->status)->id );
2568 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2570 return $self->bail_on_events($self->editor->event)
2571 unless $self->editor->create_action_transit_copy($transit);
2573 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2575 $self->checkin_changed(1);
2579 sub hold_capture_is_possible {
2581 my $copy = $self->copy;
2583 # we've been explicitly told not to capture any holds
2584 return 0 if $self->capture eq 'nocapture';
2586 # See if this copy can fulfill any holds
2587 my $hold = $holdcode->find_nearest_permitted_hold(
2588 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2590 return undef if ref $hold eq "HASH" and
2591 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2595 sub reservation_capture_is_possible {
2597 my $copy = $self->copy;
2599 # we've been explicitly told not to capture any holds
2600 return 0 if $self->capture eq 'nocapture';
2602 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2603 my $resv = $booking_ses->request(
2604 "open-ils.booking.reservations.could_capture",
2605 $self->editor->authtoken, $copy->barcode
2607 $booking_ses->disconnect;
2608 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2609 $self->push_events($resv);
2615 # returns true if the item was used (or may potentially be used
2616 # in subsequent calls) to capture a hold.
2617 sub attempt_checkin_hold_capture {
2619 my $copy = $self->copy;
2621 # we've been explicitly told not to capture any holds
2622 return 0 if $self->capture eq 'nocapture';
2624 # See if this copy can fulfill any holds
2625 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2626 $self->editor, $copy, $self->editor->requestor );
2629 $logger->debug("circulator: no potential permitted".
2630 "holds found for copy ".$copy->barcode);
2634 if($self->capture ne 'capture') {
2635 # see if this item is in a hold-capture-delay location
2636 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2637 if($U->is_true($location->hold_verify)) {
2638 $self->bail_on_events(
2639 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2644 $self->retarget($retarget);
2646 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2648 $hold->current_copy($copy->id);
2649 $hold->capture_time('now');
2650 $self->put_hold_on_shelf($hold)
2651 if $hold->pickup_lib == $self->circ_lib;
2653 # prevent DB errors caused by fetching
2654 # holds from storage, and updating through cstore
2655 $hold->clear_fulfillment_time;
2656 $hold->clear_fulfillment_staff;
2657 $hold->clear_fulfillment_lib;
2658 $hold->clear_expire_time;
2659 $hold->clear_cancel_time;
2660 $hold->clear_prev_check_time unless $hold->prev_check_time;
2662 $self->bail_on_events($self->editor->event)
2663 unless $self->editor->update_action_hold_request($hold);
2665 $self->checkin_changed(1);
2667 return 0 if $self->bail_out;
2669 if( $hold->pickup_lib == $self->circ_lib ) {
2671 # This hold was captured in the correct location
2672 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2673 $self->push_events(OpenILS::Event->new('SUCCESS'));
2675 #$self->do_hold_notify($hold->id);
2676 $self->notify_hold($hold->id);
2680 # Hold needs to be picked up elsewhere. Build a hold
2681 # transit and route the item.
2682 $self->checkin_build_hold_transit();
2683 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2684 return 0 if $self->bail_out;
2685 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2688 # make sure we save the copy status
2693 sub attempt_checkin_reservation_capture {
2695 my $copy = $self->copy;
2697 # we've been explicitly told not to capture any holds
2698 return 0 if $self->capture eq 'nocapture';
2700 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2701 my $evt = $booking_ses->request(
2702 "open-ils.booking.resources.capture_for_reservation",
2703 $self->editor->authtoken,
2705 1 # don't update copy - we probably have it locked
2707 $booking_ses->disconnect;
2709 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2711 "open-ils.booking.resources.capture_for_reservation " .
2712 "didn't return an event!"
2716 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2717 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2719 # not-transferable is an error event we'll pass on the user
2720 $logger->warn("reservation capture attempted against non-transferable item");
2721 $self->push_events($evt);
2723 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2724 # Re-retrieve copy as reservation capture may have changed
2725 # its status and whatnot.
2727 "circulator: booking capture win on copy " . $self->copy->id
2729 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2731 "circulator: changing copy " . $self->copy->id .
2732 "'s status from " . $self->copy->status . " to " .
2735 $self->copy->status($new_copy_status);
2738 $self->reservation($evt->{"payload"}->{"reservation"});
2740 if (exists $evt->{"payload"}->{"transit"}) {
2744 "org" => $evt->{"payload"}->{"transit"}->dest
2748 $self->checkin_changed(1);
2752 # other results are treated as "nothing to capture"
2756 sub do_hold_notify {
2757 my( $self, $holdid ) = @_;
2759 my $e = new_editor(xact => 1);
2760 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2762 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2763 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2765 $logger->info("circulator: running delayed hold notify process");
2767 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2768 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2770 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2771 hold_id => $holdid, requestor => $self->editor->requestor);
2773 $logger->debug("circulator: built hold notifier");
2775 if(!$notifier->event) {
2777 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2779 my $stat = $notifier->send_email_notify;
2780 if( $stat == '1' ) {
2781 $logger->info("circulator: hold notify succeeded for hold $holdid");
2785 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2788 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2792 sub retarget_holds {
2794 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2795 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2796 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2797 # no reason to wait for the return value
2801 sub checkin_build_hold_transit {
2804 my $copy = $self->copy;
2805 my $hold = $self->hold;
2806 my $trans = Fieldmapper::action::hold_transit_copy->new;
2808 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2810 $trans->hold($hold->id);
2811 $trans->source($self->circ_lib);
2812 $trans->dest($hold->pickup_lib);
2813 $trans->source_send_time("now");
2814 $trans->target_copy($copy->id);
2816 # when the copy gets to its destination, it will recover
2817 # this status - put it onto the holds shelf
2818 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2820 return $self->bail_on_events($self->editor->event)
2821 unless $self->editor->create_action_hold_transit_copy($trans);
2826 sub process_received_transit {
2828 my $copy = $self->copy;
2829 my $copyid = $self->copy->id;
2831 my $status_name = $U->copy_status($copy->status)->name;
2832 $logger->debug("circulator: attempting transit receive on ".
2833 "copy $copyid. Copy status is $status_name");
2835 my $transit = $self->transit;
2837 if( $transit->dest != $self->circ_lib ) {
2838 # - this item is in-transit to a different location
2840 my $tid = $transit->id;
2841 my $loc = $self->circ_lib;
2842 my $dest = $transit->dest;
2844 $logger->info("circulator: Fowarding transit on copy which is destined ".
2845 "for a different location. transit=$tid, copy=$copyid, current ".
2846 "location=$loc, destination location=$dest");
2848 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2850 # grab the associated hold object if available
2851 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2852 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2854 return $self->bail_on_events($evt);
2857 # The transit is received, set the receive time
2858 $transit->dest_recv_time('now');
2859 $self->bail_on_events($self->editor->event)
2860 unless $self->editor->update_action_transit_copy($transit);
2862 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2864 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2865 $copy->status( $transit->copy_status );
2866 $self->update_copy();
2867 return if $self->bail_out;
2871 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2873 # hold has arrived at destination, set shelf time
2874 $self->put_hold_on_shelf($hold);
2875 $self->bail_on_events($self->editor->event)
2876 unless $self->editor->update_action_hold_request($hold);
2877 return if $self->bail_out;
2879 $self->notify_hold($hold_transit->hold);
2884 OpenILS::Event->new(
2887 payload => { transit => $transit, holdtransit => $hold_transit } ));
2889 return $hold_transit;
2893 # ------------------------------------------------------------------
2894 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2895 # ------------------------------------------------------------------
2896 sub put_hold_on_shelf {
2897 my($self, $hold) = @_;
2899 $hold->shelf_time('now');
2901 my $shelf_expire = $U->ou_ancestor_setting_value(
2902 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2905 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2906 my $expire_time = DateTime->now->add(seconds => $seconds);
2907 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2915 sub generate_fines {
2917 my $reservation = shift;
2919 $self->generate_fines_start($reservation);
2920 $self->generate_fines_finish($reservation);
2925 sub generate_fines_start {
2927 my $reservation = shift;
2929 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2931 if (!exists($self->{_gen_fines_req})) {
2932 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
2934 'open-ils.storage.action.circulation.overdue.generate_fines',
2943 sub generate_fines_finish {
2945 my $reservation = shift;
2947 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2949 $self->{_gen_fines_req}->wait_complete;
2950 delete($self->{_gen_fines_req});
2952 # refresh the circ in case the fine generator set the stop_fines field
2953 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2954 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2959 sub checkin_handle_circ {
2961 my $circ = $self->circ;
2962 my $copy = $self->copy;
2966 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2968 # backdate the circ if necessary
2969 if($self->backdate) {
2970 my $evt = $self->checkin_handle_backdate;
2971 return $self->bail_on_events($evt) if $evt;
2974 if(!$circ->stop_fines) {
2975 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2976 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2977 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2978 $circ->stop_fines_time('now');
2979 $circ->stop_fines_time($self->backdate) if $self->backdate;
2982 # Set the checkin vars since we have the item
2983 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2985 # capture the true scan time for back-dated checkins
2986 $circ->checkin_scan_time('now');
2988 $circ->checkin_staff($self->editor->requestor->id);
2989 $circ->checkin_lib($self->circ_lib);
2990 $circ->checkin_workstation($self->editor->requestor->wsid);
2992 my $circ_lib = (ref $self->copy->circ_lib) ?
2993 $self->copy->circ_lib->id : $self->copy->circ_lib;
2994 my $stat = $U->copy_status($self->copy->status)->id;
2996 # immediately available keeps items lost or missing items from going home before being handled
2997 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2998 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3001 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3003 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3004 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3006 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3010 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3012 $self->checkin_handle_lost($circ_lib);
3016 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3021 # see if there are any fines owed on this circ. if not, close it
3022 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3023 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3025 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3027 return $self->bail_on_events($self->editor->event)
3028 unless $self->editor->update_action_circulation($circ);
3034 # ------------------------------------------------------------------
3035 # See if we need to void billings for lost checkin
3036 # ------------------------------------------------------------------
3037 sub checkin_handle_lost {
3039 my $circ_lib = shift;
3040 my $circ = $self->circ;
3042 my $max_return = $U->ou_ancestor_setting_value(
3043 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3048 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3049 $tm[5] -= 1 if $tm[5] > 0;
3050 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3052 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3053 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3055 $max_return = 0 if $today < $last_chance;
3058 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3060 my $void_lost = $U->ou_ancestor_setting_value(
3061 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3062 my $void_lost_fee = $U->ou_ancestor_setting_value(
3063 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3064 my $restore_od = $U->ou_ancestor_setting_value(
3065 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3067 $self->checkin_handle_lost_now_found(3) if $void_lost;
3068 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3069 $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3072 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3077 sub checkin_handle_backdate {
3080 # ------------------------------------------------------------------
3081 # clean up the backdate for date comparison
3082 # XXX We are currently taking the due-time from the original due-date,
3083 # not the input. Do we need to do this? This certainly interferes with
3084 # backdating of hourly checkouts, but that is likely a very rare case.
3085 # ------------------------------------------------------------------
3086 my $bd = cleanse_ISO8601($self->backdate);
3087 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3088 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3089 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3091 $self->backdate($bd);
3096 sub check_checkin_copy_status {
3098 my $copy = $self->copy;
3100 my $status = $U->copy_status($copy->status)->id;
3103 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3104 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3105 $status == OILS_COPY_STATUS_IN_PROCESS ||
3106 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3107 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3108 $status == OILS_COPY_STATUS_CATALOGING ||
3109 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3110 $status == OILS_COPY_STATUS_RESHELVING );
3112 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3113 if( $status == OILS_COPY_STATUS_LOST );
3115 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3116 if( $status == OILS_COPY_STATUS_MISSING );
3118 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3123 # --------------------------------------------------------------------------
3124 # On checkin, we need to return as many relevant objects as we can
3125 # --------------------------------------------------------------------------
3126 sub checkin_flesh_events {
3129 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3130 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3131 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3134 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3137 if($self->hold and !$self->hold->cancel_time) {
3138 $hold = $self->hold;
3139 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3143 # if we checked in a circulation, flesh the billing summary data
3144 $self->circ->billable_transaction(
3145 $self->editor->retrieve_money_billable_transaction([
3147 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3153 # flesh some patron fields before returning
3155 $self->editor->retrieve_actor_user([
3160 au => ['card', 'billing_address', 'mailing_address']
3167 for my $evt (@{$self->events}) {
3170 $payload->{copy} = $U->unflesh_copy($self->copy);
3171 $payload->{record} = $record,
3172 $payload->{circ} = $self->circ;
3173 $payload->{transit} = $self->transit;
3174 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3175 $payload->{hold} = $hold;
3176 $payload->{patron} = $self->patron;
3177 $payload->{reservation} = $self->reservation
3178 unless (not $self->reservation or $self->reservation->cancel_time);
3180 $evt->{payload} = $payload;
3185 my( $self, $msg ) = @_;
3186 my $bc = ($self->copy) ? $self->copy->barcode :
3189 my $usr = ($self->patron) ? $self->patron->id : "";
3190 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3191 ", recipient=$usr, copy=$bc");
3197 $self->log_me("do_renew()");
3199 # Make sure there is an open circ to renew that is not
3200 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3201 my $usrid = $self->patron->id if $self->patron;
3202 my $circ = $self->editor->search_action_circulation({
3203 target_copy => $self->copy->id,
3204 xact_finish => undef,
3205 ($usrid ? (usr => $usrid) : ()),
3207 {stop_fines => undef},
3208 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3212 return $self->bail_on_events($self->editor->event) unless $circ;
3214 # A user is not allowed to renew another user's items without permission
3215 unless( $circ->usr eq $self->editor->requestor->id ) {
3216 return $self->bail_on_events($self->editor->events)
3217 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3220 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3221 if $circ->renewal_remaining < 1;
3223 # -----------------------------------------------------------------
3225 $self->parent_circ($circ->id);
3226 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3229 # Run the fine generator against the old circ
3230 $self->generate_fines_start;
3232 $self->run_renew_permit;
3235 $self->do_checkin();
3236 return if $self->bail_out;
3238 unless( $self->permit_override ) {
3240 return if $self->bail_out;
3241 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3242 $self->remove_event('ITEM_NOT_CATALOGED');
3245 $self->override_events;
3246 return if $self->bail_out;
3249 $self->do_checkout();
3254 my( $self, $evt ) = @_;
3255 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3256 $logger->debug("circulator: removing event from list: $evt");
3257 my @events = @{$self->events};
3258 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3263 my( $self, $evt ) = @_;
3264 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3265 return grep { $_->{textcode} eq $evt } @{$self->events};
3270 sub run_renew_permit {
3273 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3274 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3275 $self->editor, $self->copy, $self->editor->requestor, 1
3277 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3280 if(!$self->legacy_script_support) {
3281 my $results = $self->run_indb_circ_test;
3282 $self->push_events($self->matrix_test_result_events)
3283 unless $self->circ_test_success;
3286 my $runner = $self->script_runner;
3288 $runner->load($self->circ_permit_renew);
3289 my $result = $runner->run or
3290 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3291 if ($result->{"events"}) {
3293 map { new OpenILS::Event($_) } @{$result->{"events"}}
3296 "circulator: circ_permit_renew for user " .
3297 $self->patron->id . " returned " .
3298 scalar(@{$result->{"events"}}) . " event(s)"
3302 $self->mk_script_runner;
3305 $logger->debug("circulator: re-creating script runner to be safe");
3309 # XXX: The primary mechanism for storing circ history is now handled
3310 # by tracking real circulation objects instead of bibs in a bucket.
3311 # However, this code is disabled by default and could be useful
3312 # some day, so may as well leave it for now.
3313 sub append_reading_list {
3317 $self->is_checkout and
3323 # verify history is globally enabled and uses the bucket mechanism
3324 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3325 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3327 return undef unless $htype and $htype eq 'bucket';
3329 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3331 # verify the patron wants to retain the hisory
3332 my $setting = $e->search_actor_user_setting(
3333 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3335 unless($setting and $setting->value) {
3340 my $bkt = $e->search_container_copy_bucket(
3341 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3346 # find the next item position
3347 my $last_item = $e->search_container_copy_bucket_item(
3348 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3349 $pos = $last_item->pos + 1 if $last_item;
3352 # create the history bucket if necessary
3353 $bkt = Fieldmapper::container::copy_bucket->new;
3354 $bkt->owner($self->patron->id);
3356 $bkt->btype('circ_history');
3358 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3361 my $item = Fieldmapper::container::copy_bucket_item->new;
3363 $item->bucket($bkt->id);
3364 $item->target_copy($self->copy->id);
3367 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3374 sub make_trigger_events {
3376 return unless $self->circ;
3377 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3378 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3379 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3384 sub checkin_handle_lost_now_found {
3385 my ($self, $bill_type) = @_;
3387 # ------------------------------------------------------------------
3388 # remove charge from patron's account if lost item is returned
3389 # ------------------------------------------------------------------
3391 my $bills = $self->editor->search_money_billing(
3393 xact => $self->circ->id,
3398 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3399 for my $bill (@$bills) {
3400 if( !$U->is_true($bill->voided) ) {
3401 $logger->info("lost item returned - voiding bill ".$bill->id);
3403 $bill->void_time('now');
3404 $bill->voider($self->editor->requestor->id);
3405 my $note = ($bill->note) ? $bill->note . "\n" : '';
3406 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3408 $self->bail_on_events($self->editor->event)
3409 unless $self->editor->update_money_billing($bill);
3414 sub checkin_handle_lost_now_found_restore_od {
3417 # ------------------------------------------------------------------
3418 # restore those overdue charges voided when item was set to lost
3419 # ------------------------------------------------------------------
3421 my $ods = $self->editor->search_money_billing(
3423 xact => $self->circ->id,
3428 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3429 for my $bill (@$ods) {
3430 if( $U->is_true($bill->voided) ) {
3431 $logger->info("lost item returned - restoring overdue ".$bill->id);
3433 $bill->clear_void_time;
3434 $bill->voider($self->editor->requestor->id);
3435 my $note = ($bill->note) ? $bill->note . "\n" : '';
3436 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3438 $self->bail_on_events($self->editor->event)
3439 unless $self->editor->update_money_billing($bill);