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','parts'], acn => ['record']}
40 my $conf = OpenSRF::Utils::SettingsClient->new;
41 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
43 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
44 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
46 my $lb = $conf->config_value( @pfx2, 'script_path' );
47 $lb = [ $lb ] unless ref($lb);
50 return unless $legacy_script_support;
52 my @pfx = ( @pfx2, "scripts" );
53 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
54 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
55 my $d = $conf->config_value( @pfx, 'circ_duration' );
56 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
57 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
58 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
60 $logger->error( "Missing circ script(s)" )
61 unless( $p and $c and $d and $f and $m and $pr );
63 $scripts{circ_permit_patron} = $p;
64 $scripts{circ_permit_copy} = $c;
65 $scripts{circ_duration} = $d;
66 $scripts{circ_recurring_fines} = $f;
67 $scripts{circ_max_fines} = $m;
68 $scripts{circ_permit_renew} = $pr;
71 "circulator: Loaded rules scripts for circ: " .
72 "circ permit patron = $p, ".
73 "circ permit copy = $c, ".
74 "circ duration = $d, ".
75 "circ recurring fines = $f, " .
76 "circ max fines = $m, ".
77 "circ renew permit = $pr. ".
79 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
83 __PACKAGE__->register_method(
84 method => "run_method",
85 api_name => "open-ils.circ.checkout.permit",
87 Determines if the given checkout can occur
88 @param authtoken The login session key
89 @param params A trailing hash of named params including
90 barcode : The copy barcode,
91 patron : The patron the checkout is occurring for,
92 renew : true or false - whether or not this is a renewal
93 @return The event that occurred during the permit check.
97 __PACKAGE__->register_method (
98 method => 'run_method',
99 api_name => 'open-ils.circ.checkout.permit.override',
100 signature => q/@see open-ils.circ.checkout.permit/,
104 __PACKAGE__->register_method(
105 method => "run_method",
106 api_name => "open-ils.circ.checkout",
109 @param authtoken The login session key
110 @param params A named hash of params including:
112 barcode If no copy is provided, the copy is retrieved via barcode
113 copyid If no copy or barcode is provide, the copy id will be use
114 patron The patron's id
115 noncat True if this is a circulation for a non-cataloted item
116 noncat_type The non-cataloged type id
117 noncat_circ_lib The location for the noncat circ.
118 precat The item has yet to be cataloged
119 dummy_title The temporary title of the pre-cataloded item
120 dummy_author The temporary authr of the pre-cataloded item
121 Default is the home org of the staff member
122 @return The SUCCESS event on success, any other event depending on the error
125 __PACKAGE__->register_method(
126 method => "run_method",
127 api_name => "open-ils.circ.checkin",
130 Generic super-method for handling all copies
131 @param authtoken The login session key
132 @param params Hash of named parameters including:
133 barcode - The copy barcode
134 force - If true, copies in bad statuses will be checked in and give good statuses
135 noop - don't capture holds or put items into transit
136 void_overdues - void all overdues for the circulation (aka amnesty)
141 __PACKAGE__->register_method(
142 method => "run_method",
143 api_name => "open-ils.circ.checkin.override",
144 signature => q/@see open-ils.circ.checkin/
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.renew.override",
150 signature => q/@see open-ils.circ.renew/,
154 __PACKAGE__->register_method(
155 method => "run_method",
156 api_name => "open-ils.circ.renew",
157 notes => <<" NOTES");
158 PARAMS( authtoken, circ => circ_id );
159 open-ils.circ.renew(login_session, circ_object);
160 Renews the provided circulation. login_session is the requestor of the
161 renewal and if the logged in user is not the same as circ->usr, then
162 the logged in user must have RENEW_CIRC permissions.
165 __PACKAGE__->register_method(
166 method => "run_method",
167 api_name => "open-ils.circ.checkout.full"
169 __PACKAGE__->register_method(
170 method => "run_method",
171 api_name => "open-ils.circ.checkout.full.override"
173 __PACKAGE__->register_method(
174 method => "run_method",
175 api_name => "open-ils.circ.reservation.pickup"
177 __PACKAGE__->register_method(
178 method => "run_method",
179 api_name => "open-ils.circ.reservation.return"
181 __PACKAGE__->register_method(
182 method => "run_method",
183 api_name => "open-ils.circ.reservation.return.override"
185 __PACKAGE__->register_method(
186 method => "run_method",
187 api_name => "open-ils.circ.checkout.inspect",
188 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
193 my( $self, $conn, $auth, $args ) = @_;
194 translate_legacy_args($args);
195 my $api = $self->api_name;
198 OpenILS::Application::Circ::Circulator->new($auth, %$args);
200 return circ_events($circulator) if $circulator->bail_out;
202 $circulator->use_booking(determine_booking_status());
204 # --------------------------------------------------------------------------
205 # First, check for a booking transit, as the barcode may not be a copy
206 # barcode, but a resource barcode, and nothing else in here will work
207 # --------------------------------------------------------------------------
209 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
210 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
211 if (@$resources) { # yes!
213 my $res_id_list = [ map { $_->id } @$resources ];
214 my $transit = $circulator->editor->search_action_reservation_transit_copy(
216 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
217 { order_by => { artc => 'source_send_time' }, limit => 1 }
219 )->[0]; # Any transit for this barcode?
221 if ($transit) { # yes! unwrap it.
223 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
224 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
226 my $success_event = new OpenILS::Event(
227 "SUCCESS", "payload" => {"reservation" => $reservation}
229 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
230 if (my $copy = $circulator->editor->search_asset_copy([
231 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
232 ])->[0]) { # got a copy
233 $copy->status( $transit->copy_status );
234 $copy->editor($circulator->editor->requestor->id);
235 $copy->edit_date('now');
236 $circulator->editor->update_asset_copy($copy);
237 $success_event->{"payload"}->{"record"} =
238 $U->record_to_mvr($copy->call_number->record);
239 $success_event->{"payload"}->{"volume"} = $copy->call_number;
240 $copy->call_number($copy->call_number->id);
241 $success_event->{"payload"}->{"copy"} = $copy;
245 $transit->dest_recv_time('now');
246 $circulator->editor->update_action_reservation_transit_copy( $transit );
248 $circulator->editor->commit;
249 # Formerly this branch just stopped here. Argh!
250 $conn->respond_complete($success_event);
258 # --------------------------------------------------------------------------
259 # Go ahead and load the script runner to make sure we have all
260 # of the objects we need
261 # --------------------------------------------------------------------------
263 if ($circulator->use_booking) {
264 $circulator->is_res_checkin($circulator->is_checkin(1))
265 if $api =~ /reservation.return/ or (
266 $api =~ /checkin/ and $circulator->seems_like_reservation()
269 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
272 $circulator->is_renewal(1) if $api =~ /renew/;
273 $circulator->is_checkin(1) if $api =~ /checkin/;
275 $circulator->mk_env();
276 $circulator->noop(1) if $circulator->claims_never_checked_out;
278 if($legacy_script_support and not $circulator->is_checkin) {
279 $circulator->mk_script_runner();
280 $circulator->legacy_script_support(1);
281 $circulator->circ_permit_patron($scripts{circ_permit_patron});
282 $circulator->circ_permit_copy($scripts{circ_permit_copy});
283 $circulator->circ_duration($scripts{circ_duration});
284 $circulator->circ_permit_renew($scripts{circ_permit_renew});
286 return circ_events($circulator) if $circulator->bail_out;
289 $circulator->override(1) if $api =~ /override/o;
291 if( $api =~ /checkout\.permit/ ) {
292 $circulator->do_permit();
294 } elsif( $api =~ /checkout.full/ ) {
296 # requesting a precat checkout implies that any required
297 # overrides have been performed. Go ahead and re-override.
298 $circulator->skip_permit_key(1);
299 $circulator->override(1) if $circulator->request_precat;
300 $circulator->do_permit();
301 $circulator->is_checkout(1);
302 unless( $circulator->bail_out ) {
303 $circulator->events([]);
304 $circulator->do_checkout();
307 } elsif( $circulator->is_res_checkout ) {
308 $circulator->do_reservation_pickup();
310 } elsif( $api =~ /inspect/ ) {
311 my $data = $circulator->do_inspect();
312 $circulator->editor->rollback;
315 } elsif( $api =~ /checkout/ ) {
316 $circulator->is_checkout(1);
317 $circulator->do_checkout();
319 } elsif( $circulator->is_res_checkin ) {
320 $circulator->do_reservation_return();
321 $circulator->do_checkin() if ($circulator->copy());
322 } elsif( $api =~ /checkin/ ) {
323 $circulator->do_checkin();
325 } elsif( $api =~ /renew/ ) {
326 $circulator->is_renewal(1);
327 $circulator->do_renew();
330 if( $circulator->bail_out ) {
333 # make sure no success event accidentally slip in
335 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
338 my @e = @{$circulator->events};
339 push( @ee, $_->{textcode} ) for @e;
340 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
342 $circulator->editor->rollback;
345 $circulator->editor->commit;
348 $circulator->script_runner->cleanup if $circulator->script_runner;
350 $conn->respond_complete(circ_events($circulator));
352 unless($circulator->bail_out) {
353 $circulator->do_hold_notify($circulator->notify_hold)
354 if $circulator->notify_hold;
355 $circulator->retarget_holds if $circulator->retarget;
356 $circulator->append_reading_list;
357 $circulator->make_trigger_events;
363 my @e = @{$circ->events};
364 # if we have multiple events, SUCCESS should not be one of them;
365 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
366 return (@e == 1) ? $e[0] : \@e;
370 sub translate_legacy_args {
373 if( $$args{barcode} ) {
374 $$args{copy_barcode} = $$args{barcode};
375 delete $$args{barcode};
378 if( $$args{copyid} ) {
379 $$args{copy_id} = $$args{copyid};
380 delete $$args{copyid};
383 if( $$args{patronid} ) {
384 $$args{patron_id} = $$args{patronid};
385 delete $$args{patronid};
388 if( $$args{patron} and !ref($$args{patron}) ) {
389 $$args{patron_id} = $$args{patron};
390 delete $$args{patron};
394 if( $$args{noncat} ) {
395 $$args{is_noncat} = $$args{noncat};
396 delete $$args{noncat};
399 if( $$args{precat} ) {
400 $$args{is_precat} = $$args{request_precat} = $$args{precat};
401 delete $$args{precat};
407 # --------------------------------------------------------------------------
408 # This package actually manages all of the circulation logic
409 # --------------------------------------------------------------------------
410 package OpenILS::Application::Circ::Circulator;
411 use strict; use warnings;
412 use vars q/$AUTOLOAD/;
414 use OpenILS::Utils::Fieldmapper;
415 use OpenSRF::Utils::Cache;
416 use Digest::MD5 qw(md5_hex);
417 use DateTime::Format::ISO8601;
418 use OpenILS::Utils::PermitHold;
419 use OpenSRF::Utils qw/:datetime/;
420 use OpenSRF::Utils::SettingsClient;
421 use OpenILS::Application::Circ::Holds;
422 use OpenILS::Application::Circ::Transit;
423 use OpenSRF::Utils::Logger qw(:logger);
424 use OpenILS::Utils::CStoreEditor qw/:funcs/;
425 use OpenILS::Application::Circ::ScriptBuilder;
426 use OpenILS::Const qw/:const/;
427 use OpenILS::Utils::Penalty;
428 use OpenILS::Application::Circ::CircCommon;
431 my $holdcode = "OpenILS::Application::Circ::Holds";
432 my $transcode = "OpenILS::Application::Circ::Transit";
438 # --------------------------------------------------------------------------
439 # Add a pile of automagic getter/setter methods
440 # --------------------------------------------------------------------------
441 my @AUTOLOAD_FIELDS = qw/
488 recurring_fines_level
501 cancelled_hold_transit
508 circ_matrix_matchpoint
510 legacy_script_support
520 claims_never_checked_out
530 my $type = ref($self) or die "$self is not an object";
532 my $name = $AUTOLOAD;
535 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
536 $logger->error("circulator: $type: invalid autoload field: $name");
537 die "$type: invalid autoload field: $name\n"
542 *{"${type}::${name}"} = sub {
545 $s->{$name} = $v if defined $v;
549 return $self->$name($data);
554 my( $class, $auth, %args ) = @_;
555 $class = ref($class) || $class;
556 my $self = bless( {}, $class );
559 $self->editor(new_editor(xact => 1, authtoken => $auth));
561 unless( $self->editor->checkauth ) {
562 $self->bail_on_events($self->editor->event);
566 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
568 $self->$_($args{$_}) for keys %args;
571 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
573 # if this is a renewal, default to desk_renewal
574 $self->desk_renewal(1) unless
575 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
577 $self->capture('') unless $self->capture;
579 unless(%user_groups) {
580 my $gps = $self->editor->retrieve_all_permission_grp_tree;
581 %user_groups = map { $_->id => $_ } @$gps;
588 # --------------------------------------------------------------------------
589 # True if we should discontinue processing
590 # --------------------------------------------------------------------------
592 my( $self, $bool ) = @_;
593 if( defined $bool ) {
594 $logger->info("circulator: BAILING OUT") if $bool;
595 $self->{bail_out} = $bool;
597 return $self->{bail_out};
602 my( $self, @evts ) = @_;
605 $e->{payload} = $self->copy if
606 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
608 $logger->info("circulator: pushing event ".$e->{textcode});
609 push( @{$self->events}, $e ) unless
610 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
616 return '' if $self->skip_permit_key;
617 my $key = md5_hex( time() . rand() . "$$" );
618 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
619 return $self->permit_key($key);
622 sub check_permit_key {
624 return 1 if $self->skip_permit_key;
625 my $key = $self->permit_key;
626 return 0 unless $key;
627 my $k = "oils_permit_key_$key";
628 my $one = $self->cache_handle->get_cache($k);
629 $self->cache_handle->delete_cache($k);
630 return ($one) ? 1 : 0;
633 sub seems_like_reservation {
636 # Some words about the following method:
637 # 1) It requires the VIEW_USER permission, but that's not an
638 # issue, right, since all staff should have that?
639 # 2) It returns only one reservation at a time, even if an item can be
640 # and is currently overbooked. Hmmm....
641 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
642 my $result = $booking_ses->request(
643 "open-ils.booking.reservations.by_returnable_resource_barcode",
644 $self->editor->authtoken,
647 $booking_ses->disconnect;
649 return $self->bail_on_events($result) if defined $U->event_code($result);
652 $self->reservation(shift @$result);
660 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
661 sub save_trimmed_copy {
662 my ($self, $copy) = @_;
665 $self->volume($copy->call_number);
666 $self->title($self->volume->record);
667 $self->copy->call_number($self->volume->id);
668 $self->volume->record($self->title->id);
669 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
670 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
671 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
672 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
678 my $e = $self->editor;
680 # --------------------------------------------------------------------------
681 # Grab the fleshed copy
682 # --------------------------------------------------------------------------
683 unless($self->is_noncat) {
686 $copy = $e->retrieve_asset_copy(
687 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
689 } elsif( $self->copy_barcode ) {
691 $copy = $e->search_asset_copy(
692 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
693 } elsif( $self->reservation ) {
694 my $res = $e->json_query(
696 "select" => {"acp" => ["id"]},
701 "field" => "barcode",
705 "field" => "current_resource"
713 "id" => (ref $self->reservation) ?
714 $self->reservation->id : $self->reservation
719 if (ref $res eq "ARRAY" and scalar @$res) {
720 $logger->info("circulator: mapped reservation " .
721 $self->reservation . " to copy " . $res->[0]->{"id"});
722 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
727 $self->save_trimmed_copy($copy);
729 # We can't renew if there is no copy
730 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
731 if $self->is_renewal;
736 # --------------------------------------------------------------------------
738 # --------------------------------------------------------------------------
742 flesh_fields => {au => [ qw/ card / ]}
745 if( $self->patron_id ) {
746 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
747 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
749 } elsif( $self->patron_barcode ) {
751 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
752 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
753 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
755 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
756 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
759 if( my $copy = $self->copy ) {
762 $flesh->{flesh_fields}->{circ} = ['usr'];
764 my $circ = $e->search_action_circulation([
765 {target_copy => $copy->id, checkin_time => undef}, $flesh
769 $patron = $circ->usr;
770 $circ->usr($patron->id); # de-flesh for consistency
776 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
777 unless $self->patron($patron) or $self->is_checkin;
779 unless($self->is_checkin) {
781 # Check for inactivity and patron reg. expiration
783 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
784 unless $U->is_true($patron->active);
786 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
787 unless $U->is_true($patron->card->active);
789 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
790 cleanse_ISO8601($patron->expire_date));
792 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
793 if( CORE::time > $expire->epoch ) ;
797 # --------------------------------------------------------------------------
798 # This builds the script runner environment and fetches most of the
800 # --------------------------------------------------------------------------
801 sub mk_script_runner {
807 qw/copy copy_barcode copy_id patron
808 patron_id patron_barcode volume title editor/;
810 # Translate our objects into the ScriptBuilder args hash
811 $$args{$_} = $self->$_() for @fields;
813 $args->{ignore_user_status} = 1 if $self->is_checkin;
814 $$args{fetch_patron_by_circ_copy} = 1;
815 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
817 if( my $pco = $self->pending_checkouts ) {
818 $logger->info("circulator: we were given a pending checkouts number of $pco");
819 $$args{patronItemsOut} = $pco;
822 # This fetches most of the objects we need
823 $self->script_runner(
824 OpenILS::Application::Circ::ScriptBuilder->build($args));
826 # Now we translate the ScriptBuilder objects back into self
827 $self->$_($$args{$_}) for @fields;
829 my @evts = @{$args->{_events}} if $args->{_events};
831 $logger->debug("circulator: script builder returned events: @evts") if @evts;
835 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
836 if(!$self->is_noncat and
838 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
842 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
843 return $self->bail_on_events(@e);
848 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
849 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
850 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
851 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
855 # We can't renew if there is no copy
856 return $self->bail_on_events(@evts) if
857 $self->is_renewal and !$self->copy;
859 # Set some circ-specific flags in the script environment
860 my $evt = "environment";
861 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
863 if( $self->is_noncat ) {
864 $self->script_runner->insert("$evt.isNonCat", 1);
865 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
868 if( $self->is_precat ) {
869 $self->script_runner->insert("environment.isPrecat", 1, 1);
872 $self->script_runner->add_path( $_ ) for @$script_libs;
877 # --------------------------------------------------------------------------
878 # Does the circ permit work
879 # --------------------------------------------------------------------------
883 $self->log_me("do_permit()");
885 unless( $self->editor->requestor->id == $self->patron->id ) {
886 return $self->bail_on_events($self->editor->event)
887 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
890 $self->check_captured_holds();
891 $self->do_copy_checks();
892 return if $self->bail_out;
893 $self->run_patron_permit_scripts();
894 $self->run_copy_permit_scripts()
895 unless $self->is_precat or $self->is_noncat;
896 $self->check_item_deposit_events();
897 $self->override_events();
898 return if $self->bail_out;
900 if($self->is_precat and not $self->request_precat) {
903 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
904 return $self->bail_out(1) unless $self->is_renewal;
908 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
911 sub check_item_deposit_events {
913 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
914 if $self->is_deposit and not $self->is_deposit_exempt;
915 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
916 if $self->is_rental and not $self->is_rental_exempt;
919 # returns true if the user is not required to pay deposits
920 sub is_deposit_exempt {
922 my $pid = (ref $self->patron->profile) ?
923 $self->patron->profile->id : $self->patron->profile;
924 my $groups = $U->ou_ancestor_setting_value(
925 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
926 for my $grp (@$groups) {
927 return 1 if $self->is_group_descendant($grp, $pid);
932 # returns true if the user is not required to pay rental fees
933 sub is_rental_exempt {
935 my $pid = (ref $self->patron->profile) ?
936 $self->patron->profile->id : $self->patron->profile;
937 my $groups = $U->ou_ancestor_setting_value(
938 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
939 for my $grp (@$groups) {
940 return 1 if $self->is_group_descendant($grp, $pid);
945 sub is_group_descendant {
946 my($self, $p_id, $c_id) = @_;
947 return 0 unless defined $p_id and defined $c_id;
948 return 1 if $c_id == $p_id;
949 while(my $grp = $user_groups{$c_id}) {
950 $c_id = $grp->parent;
951 return 0 unless defined $c_id;
952 return 1 if $c_id == $p_id;
957 sub check_captured_holds {
959 my $copy = $self->copy;
960 my $patron = $self->patron;
962 return undef unless $copy;
964 my $s = $U->copy_status($copy->status)->id;
965 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
966 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
968 # Item is on the holds shelf, make sure it's going to the right person
969 my $holds = $self->editor->search_action_hold_request(
972 current_copy => $copy->id ,
973 capture_time => { '!=' => undef },
974 cancel_time => undef,
975 fulfillment_time => undef
981 if( $holds and $$holds[0] ) {
982 return undef if $$holds[0]->usr == $patron->id;
985 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
987 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
993 my $copy = $self->copy;
996 my $stat = $U->copy_status($copy->status)->id;
998 # We cannot check out a copy if it is in-transit
999 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1000 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1003 $self->handle_claims_returned();
1004 return if $self->bail_out;
1006 # no claims returned circ was found, check if there is any open circ
1007 unless( $self->is_renewal ) {
1009 my $circs = $self->editor->search_action_circulation(
1010 { target_copy => $copy->id, checkin_time => undef }
1013 if(my $old_circ = $circs->[0]) { # an open circ was found
1015 my $payload = {copy => $copy};
1017 if($old_circ->usr == $self->patron->id) {
1019 $payload->{old_circ} = $old_circ;
1021 # If there is an open circulation on the checkout item and an auto-renew
1022 # interval is defined, inform the caller that they should go
1023 # ahead and renew the item instead of warning about open circulations.
1025 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1027 'circ.checkout_auto_renew_age',
1031 if($auto_renew_intvl) {
1032 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1033 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1035 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1036 $payload->{auto_renew} = 1;
1041 return $self->bail_on_events(
1042 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1048 my $LEGACY_CIRC_EVENT_MAP = {
1049 'no_item' => 'ITEM_NOT_CATALOGED',
1050 'actor.usr.barred' => 'PATRON_BARRED',
1051 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1052 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1053 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1054 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1055 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1056 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1057 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1058 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1062 # ---------------------------------------------------------------------
1063 # This pushes any patron-related events into the list but does not
1064 # set bail_out for any events
1065 # ---------------------------------------------------------------------
1066 sub run_patron_permit_scripts {
1068 my $runner = $self->script_runner;
1069 my $patronid = $self->patron->id;
1073 if(!$self->legacy_script_support) {
1075 my $results = $self->run_indb_circ_test;
1076 unless($self->circ_test_success) {
1077 # no_item result is OK during noncat checkout
1078 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1079 push @allevents, $self->matrix_test_result_events;
1085 # ---------------------------------------------------------------------
1086 # # Now run the patron permit script
1087 # ---------------------------------------------------------------------
1088 $runner->load($self->circ_permit_patron);
1089 my $result = $runner->run or
1090 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1092 my $patron_events = $result->{events};
1094 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1095 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1096 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1097 $penalties = $penalties->{fatal_penalties};
1099 for my $pen (@$penalties) {
1100 my $event = OpenILS::Event->new($pen->name);
1101 $event->{desc} = $pen->label;
1102 push(@allevents, $event);
1105 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1109 $_->{payload} = $self->copy if
1110 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1113 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1115 $self->push_events(@allevents);
1118 sub matrix_test_result_codes {
1120 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1123 sub matrix_test_result_events {
1126 my $event = new OpenILS::Event(
1127 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1129 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1131 } (@{$self->matrix_test_result});
1134 sub run_indb_circ_test {
1136 return $self->matrix_test_result if $self->matrix_test_result;
1138 my $dbfunc = ($self->is_renewal) ?
1139 'action.item_user_renew_test' : 'action.item_user_circ_test';
1141 if( $self->is_precat && $self->request_precat) {
1142 $self->make_precat_copy;
1143 return if $self->bail_out;
1146 my $results = $self->editor->json_query(
1150 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1156 $self->circ_test_success($U->is_true($results->[0]->{success}));
1158 if(my $mp = $results->[0]->{matchpoint}) {
1159 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1160 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1161 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1162 if($results->[0]->{renewals}) {
1163 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1165 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1166 if($results->[0]->{grace_period}) {
1167 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1169 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1170 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1173 return $self->matrix_test_result($results);
1176 # ---------------------------------------------------------------------
1177 # given a use and copy, this will calculate the circulation policy
1178 # parameters. Only works with in-db circ.
1179 # ---------------------------------------------------------------------
1183 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1185 $self->run_indb_circ_test;
1188 circ_test_success => $self->circ_test_success,
1189 failure_events => [],
1190 failure_codes => [],
1191 matchpoint => $self->circ_matrix_matchpoint
1194 unless($self->circ_test_success) {
1195 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1196 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1199 if($self->circ_matrix_matchpoint) {
1200 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1201 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1202 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1203 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1205 my $policy = $self->get_circ_policy(
1206 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1208 $$results{$_} = $$policy{$_} for keys %$policy;
1214 # ---------------------------------------------------------------------
1215 # Loads the circ policy info for duration, recurring fine, and max
1216 # fine based on the current copy
1217 # ---------------------------------------------------------------------
1218 sub get_circ_policy {
1219 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1222 duration_rule => $duration_rule->name,
1223 recurring_fine_rule => $recurring_fine_rule->name,
1224 max_fine_rule => $max_fine_rule->name,
1225 max_fine => $self->get_max_fine_amount($max_fine_rule),
1226 fine_interval => $recurring_fine_rule->recurrence_interval,
1227 renewal_remaining => $duration_rule->max_renewals,
1228 grace_period => $recurring_fine_rule->grace_period
1231 if($hard_due_date) {
1232 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1233 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1236 $policy->{duration_date_ceiling} = undef;
1237 $policy->{duration_date_ceiling_force} = undef;
1240 $policy->{duration} = $duration_rule->shrt
1241 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1242 $policy->{duration} = $duration_rule->normal
1243 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1244 $policy->{duration} = $duration_rule->extended
1245 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1247 $policy->{recurring_fine} = $recurring_fine_rule->low
1248 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1249 $policy->{recurring_fine} = $recurring_fine_rule->normal
1250 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1251 $policy->{recurring_fine} = $recurring_fine_rule->high
1252 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1257 sub get_max_fine_amount {
1259 my $max_fine_rule = shift;
1260 my $max_amount = $max_fine_rule->amount;
1262 # if is_percent is true then the max->amount is
1263 # use as a percentage of the copy price
1264 if ($U->is_true($max_fine_rule->is_percent)) {
1265 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1266 $max_amount = $price * $max_fine_rule->amount / 100;
1268 $U->ou_ancestor_setting_value(
1270 'circ.max_fine.cap_at_price',
1274 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1275 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1283 sub run_copy_permit_scripts {
1285 my $copy = $self->copy || return;
1286 my $runner = $self->script_runner;
1290 if(!$self->legacy_script_support) {
1291 my $results = $self->run_indb_circ_test;
1292 push @allevents, $self->matrix_test_result_events
1293 unless $self->circ_test_success;
1296 # ---------------------------------------------------------------------
1297 # Capture all of the copy permit events
1298 # ---------------------------------------------------------------------
1299 $runner->load($self->circ_permit_copy);
1300 my $result = $runner->run or
1301 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1302 my $copy_events = $result->{events};
1304 # ---------------------------------------------------------------------
1305 # Now collect all of the events together
1306 # ---------------------------------------------------------------------
1307 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1310 # See if this copy has an alert message
1311 my $ae = $self->check_copy_alert();
1312 push( @allevents, $ae ) if $ae;
1314 # uniquify the events
1315 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1316 @allevents = values %hash;
1318 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1320 $self->push_events(@allevents);
1324 sub check_copy_alert {
1326 return undef if $self->is_renewal;
1327 return OpenILS::Event->new(
1328 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1329 if $self->copy and $self->copy->alert_message;
1335 # --------------------------------------------------------------------------
1336 # If the call is overriding and has permissions to override every collected
1337 # event, the are cleared. Any event that the caller does not have
1338 # permission to override, will be left in the event list and bail_out will
1340 # XXX We need code in here to cancel any holds/transits on copies
1341 # that are being force-checked out
1342 # --------------------------------------------------------------------------
1343 sub override_events {
1345 my @events = @{$self->events};
1346 return unless @events;
1348 if(!$self->override) {
1349 return $self->bail_out(1)
1350 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1355 for my $e (@events) {
1356 my $tc = $e->{textcode};
1357 next if $tc eq 'SUCCESS';
1358 my $ov = "$tc.override";
1359 $logger->info("circulator: attempting to override event: $ov");
1361 return $self->bail_on_events($self->editor->event)
1362 unless( $self->editor->allowed($ov) );
1367 # --------------------------------------------------------------------------
1368 # If there is an open claimsreturn circ on the requested copy, close the
1369 # circ if overriding, otherwise bail out
1370 # --------------------------------------------------------------------------
1371 sub handle_claims_returned {
1373 my $copy = $self->copy;
1375 my $CR = $self->editor->search_action_circulation(
1377 target_copy => $copy->id,
1378 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1379 checkin_time => undef,
1383 return unless ($CR = $CR->[0]);
1387 # - If the caller has set the override flag, we will check the item in
1388 if($self->override) {
1390 $CR->checkin_time('now');
1391 $CR->checkin_scan_time('now');
1392 $CR->checkin_lib($self->circ_lib);
1393 $CR->checkin_workstation($self->editor->requestor->wsid);
1394 $CR->checkin_staff($self->editor->requestor->id);
1396 $evt = $self->editor->event
1397 unless $self->editor->update_action_circulation($CR);
1400 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1403 $self->bail_on_events($evt) if $evt;
1408 # --------------------------------------------------------------------------
1409 # This performs the checkout
1410 # --------------------------------------------------------------------------
1414 $self->log_me("do_checkout()");
1416 # make sure perms are good if this isn't a renewal
1417 unless( $self->is_renewal ) {
1418 return $self->bail_on_events($self->editor->event)
1419 unless( $self->editor->allowed('COPY_CHECKOUT') );
1422 # verify the permit key
1423 unless( $self->check_permit_key ) {
1424 if( $self->permit_override ) {
1425 return $self->bail_on_events($self->editor->event)
1426 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1428 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1432 # if this is a non-cataloged circ, build the circ and finish
1433 if( $self->is_noncat ) {
1434 $self->checkout_noncat;
1436 OpenILS::Event->new('SUCCESS',
1437 payload => { noncat_circ => $self->circ }));
1441 if( $self->is_precat ) {
1442 $self->make_precat_copy;
1443 return if $self->bail_out;
1445 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1446 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1449 $self->do_copy_checks;
1450 return if $self->bail_out;
1452 $self->run_checkout_scripts();
1453 return if $self->bail_out;
1455 $self->build_checkout_circ_object();
1456 return if $self->bail_out;
1458 my $modify_to_start = $self->booking_adjusted_due_date();
1459 return if $self->bail_out;
1461 $self->apply_modified_due_date($modify_to_start);
1462 return if $self->bail_out;
1464 return $self->bail_on_events($self->editor->event)
1465 unless $self->editor->create_action_circulation($self->circ);
1467 # refresh the circ to force local time zone for now
1468 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1470 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1472 return if $self->bail_out;
1474 $self->apply_deposit_fee();
1475 return if $self->bail_out;
1477 $self->handle_checkout_holds();
1478 return if $self->bail_out;
1480 # ------------------------------------------------------------------------------
1481 # Update the patron penalty info in the DB. Run it for permit-overrides
1482 # since the penalties are not updated during the permit phase
1483 # ------------------------------------------------------------------------------
1484 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1486 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1489 if($self->is_renewal) {
1490 # flesh the billing summary for the checked-in circ
1491 $pcirc = $self->editor->retrieve_action_circulation([
1493 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1498 OpenILS::Event->new('SUCCESS',
1500 copy => $U->unflesh_copy($self->copy),
1501 volume => $self->volume,
1502 circ => $self->circ,
1504 holds_fulfilled => $self->fulfilled_holds,
1505 deposit_billing => $self->deposit_billing,
1506 rental_billing => $self->rental_billing,
1507 parent_circ => $pcirc,
1508 patron => ($self->return_patron) ? $self->patron : undef,
1509 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1515 sub apply_deposit_fee {
1517 my $copy = $self->copy;
1519 ($self->is_deposit and not $self->is_deposit_exempt) or
1520 ($self->is_rental and not $self->is_rental_exempt);
1522 return if $self->is_deposit and $self->skip_deposit_fee;
1523 return if $self->is_rental and $self->skip_rental_fee;
1525 my $bill = Fieldmapper::money::billing->new;
1526 my $amount = $copy->deposit_amount;
1530 if($self->is_deposit) {
1531 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1533 $self->deposit_billing($bill);
1535 $billing_type = OILS_BILLING_TYPE_RENTAL;
1537 $self->rental_billing($bill);
1540 $bill->xact($self->circ->id);
1541 $bill->amount($amount);
1542 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1543 $bill->billing_type($billing_type);
1544 $bill->btype($btype);
1545 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1547 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1552 my $copy = $self->copy;
1554 my $stat = $copy->status if ref $copy->status;
1555 my $loc = $copy->location if ref $copy->location;
1556 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1558 $copy->status($stat->id) if $stat;
1559 $copy->location($loc->id) if $loc;
1560 $copy->circ_lib($circ_lib->id) if $circ_lib;
1561 $copy->editor($self->editor->requestor->id);
1562 $copy->edit_date('now');
1563 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1565 return $self->bail_on_events($self->editor->event)
1566 unless $self->editor->update_asset_copy($self->copy);
1568 $copy->status($U->copy_status($copy->status));
1569 $copy->location($loc) if $loc;
1570 $copy->circ_lib($circ_lib) if $circ_lib;
1573 sub update_reservation {
1575 my $reservation = $self->reservation;
1577 my $usr = $reservation->usr;
1578 my $target_rt = $reservation->target_resource_type;
1579 my $target_r = $reservation->target_resource;
1580 my $current_r = $reservation->current_resource;
1582 $reservation->usr($usr->id) if ref $usr;
1583 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1584 $reservation->target_resource($target_r->id) if ref $target_r;
1585 $reservation->current_resource($current_r->id) if ref $current_r;
1587 return $self->bail_on_events($self->editor->event)
1588 unless $self->editor->update_booking_reservation($self->reservation);
1591 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1592 $self->reservation($reservation);
1596 sub bail_on_events {
1597 my( $self, @evts ) = @_;
1598 $self->push_events(@evts);
1603 # ------------------------------------------------------------------------------
1604 # When an item is checked out, see if we can fulfill a hold for this patron
1605 # ------------------------------------------------------------------------------
1606 sub handle_checkout_holds {
1608 my $copy = $self->copy;
1609 my $patron = $self->patron;
1611 my $e = $self->editor;
1612 $self->fulfilled_holds([]);
1614 # pre/non-cats can't fulfill a hold
1615 return if $self->is_precat or $self->is_noncat;
1617 my $hold = $e->search_action_hold_request({
1618 current_copy => $copy->id ,
1619 cancel_time => undef,
1620 fulfillment_time => undef,
1622 {expire_time => undef},
1623 {expire_time => {'>' => 'now'}}
1627 if($hold and $hold->usr != $patron->id) {
1628 # reset the hold since the copy is now checked out
1630 $logger->info("circulator: un-targeting hold ".$hold->id.
1631 " because copy ".$copy->id." is getting checked out");
1633 $hold->clear_prev_check_time;
1634 $hold->clear_current_copy;
1635 $hold->clear_capture_time;
1637 return $self->bail_on_event($e->event)
1638 unless $e->update_action_hold_request($hold);
1644 $hold = $self->find_related_user_hold($copy, $patron) or return;
1645 $logger->info("circulator: found related hold to fulfill in checkout");
1648 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1650 # if the hold was never officially captured, capture it.
1651 $hold->current_copy($copy->id);
1652 $hold->capture_time('now') unless $hold->capture_time;
1653 $hold->fulfillment_time('now');
1654 $hold->fulfillment_staff($e->requestor->id);
1655 $hold->fulfillment_lib($self->circ_lib);
1657 return $self->bail_on_events($e->event)
1658 unless $e->update_action_hold_request($hold);
1660 $holdcode->delete_hold_copy_maps($e, $hold->id);
1661 return $self->fulfilled_holds([$hold->id]);
1665 # ------------------------------------------------------------------------------
1666 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1667 # the patron directly targets the checked out item, see if there is another hold
1668 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1669 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1670 # ------------------------------------------------------------------------------
1671 sub find_related_user_hold {
1672 my($self, $copy, $patron) = @_;
1673 my $e = $self->editor;
1675 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1677 return undef unless $U->ou_ancestor_setting_value(
1678 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1680 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1682 select => {ahr => ['id']},
1687 fkey => 'current_copy',
1688 type => 'left' # there may be no current_copy
1695 fulfillment_time => undef,
1696 cancel_time => undef,
1698 {expire_time => undef},
1699 {expire_time => {'>' => 'now'}}
1706 target => $self->volume->id
1712 target => $self->title->id
1718 {id => undef}, # left-join copy may be nonexistent
1719 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1723 order_by => {ahr => {request_time => {direction => 'asc'}}},
1727 my $hold_info = $e->json_query($args)->[0];
1728 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1733 sub run_checkout_scripts {
1738 my $runner = $self->script_runner;
1747 my $hard_due_date_name;
1749 if(!$self->legacy_script_support) {
1750 $self->run_indb_circ_test();
1751 $duration = $self->circ_matrix_matchpoint->duration_rule;
1752 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1753 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1754 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1758 $runner->load($self->circ_duration);
1760 my $result = $runner->run or
1761 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1763 $duration_name = $result->{durationRule};
1764 $recurring_name = $result->{recurringFinesRule};
1765 $max_fine_name = $result->{maxFine};
1766 $hard_due_date_name = $result->{hardDueDate};
1769 $duration_name = $duration->name if $duration;
1770 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1773 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1774 return $self->bail_on_events($evt) if ($evt && !$nobail);
1776 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1777 return $self->bail_on_events($evt) if ($evt && !$nobail);
1779 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1780 return $self->bail_on_events($evt) if ($evt && !$nobail);
1782 if($hard_due_date_name) {
1783 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1784 return $self->bail_on_events($evt) if ($evt && !$nobail);
1790 # The item circulates with an unlimited duration
1794 $hard_due_date = undef;
1797 $self->duration_rule($duration);
1798 $self->recurring_fines_rule($recurring);
1799 $self->max_fine_rule($max_fine);
1800 $self->hard_due_date($hard_due_date);
1804 sub build_checkout_circ_object {
1807 my $circ = Fieldmapper::action::circulation->new;
1808 my $duration = $self->duration_rule;
1809 my $max = $self->max_fine_rule;
1810 my $recurring = $self->recurring_fines_rule;
1811 my $hard_due_date = $self->hard_due_date;
1812 my $copy = $self->copy;
1813 my $patron = $self->patron;
1814 my $duration_date_ceiling;
1815 my $duration_date_ceiling_force;
1819 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1820 $duration_date_ceiling = $policy->{duration_date_ceiling};
1821 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1823 my $dname = $duration->name;
1824 my $mname = $max->name;
1825 my $rname = $recurring->name;
1827 if($hard_due_date) {
1828 $hdname = $hard_due_date->name;
1831 $logger->debug("circulator: building circulation ".
1832 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1834 $circ->duration($policy->{duration});
1835 $circ->recurring_fine($policy->{recurring_fine});
1836 $circ->duration_rule($duration->name);
1837 $circ->recurring_fine_rule($recurring->name);
1838 $circ->max_fine_rule($max->name);
1839 $circ->max_fine($policy->{max_fine});
1840 $circ->fine_interval($recurring->recurrence_interval);
1841 $circ->renewal_remaining($duration->max_renewals);
1842 $circ->grace_period($policy->{grace_period});
1846 $logger->info("circulator: copy found with an unlimited circ duration");
1847 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1848 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1849 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1850 $circ->renewal_remaining(0);
1851 $circ->grace_period(0);
1854 $circ->target_copy( $copy->id );
1855 $circ->usr( $patron->id );
1856 $circ->circ_lib( $self->circ_lib );
1857 $circ->workstation($self->editor->requestor->wsid)
1858 if defined $self->editor->requestor->wsid;
1860 # renewals maintain a link to the parent circulation
1861 $circ->parent_circ($self->parent_circ);
1863 if( $self->is_renewal ) {
1864 $circ->opac_renewal('t') if $self->opac_renewal;
1865 $circ->phone_renewal('t') if $self->phone_renewal;
1866 $circ->desk_renewal('t') if $self->desk_renewal;
1867 $circ->renewal_remaining($self->renewal_remaining);
1868 $circ->circ_staff($self->editor->requestor->id);
1872 # if the user provided an overiding checkout time,
1873 # (e.g. the checkout really happened several hours ago), then
1874 # we apply that here. Does this need a perm??
1875 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1876 if $self->checkout_time;
1878 # if a patron is renewing, 'requestor' will be the patron
1879 $circ->circ_staff($self->editor->requestor->id);
1880 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1885 sub do_reservation_pickup {
1888 $self->log_me("do_reservation_pickup()");
1890 $self->reservation->pickup_time('now');
1893 $self->reservation->current_resource &&
1894 $U->is_true($self->reservation->target_resource_type->catalog_item)
1896 # We used to try to set $self->copy and $self->patron here,
1897 # but that should already be done.
1899 $self->run_checkout_scripts(1);
1901 my $duration = $self->duration_rule;
1902 my $max = $self->max_fine_rule;
1903 my $recurring = $self->recurring_fines_rule;
1905 if ($duration && $max && $recurring) {
1906 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1908 my $dname = $duration->name;
1909 my $mname = $max->name;
1910 my $rname = $recurring->name;
1912 $logger->debug("circulator: updating reservation ".
1913 "with duration=$dname, maxfine=$mname, recurring=$rname");
1915 $self->reservation->fine_amount($policy->{recurring_fine});
1916 $self->reservation->max_fine($policy->{max_fine});
1917 $self->reservation->fine_interval($recurring->recurrence_interval);
1920 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1921 $self->update_copy();
1924 $self->reservation->fine_amount(
1925 $self->reservation->target_resource_type->fine_amount
1927 $self->reservation->max_fine(
1928 $self->reservation->target_resource_type->max_fine
1930 $self->reservation->fine_interval(
1931 $self->reservation->target_resource_type->fine_interval
1935 $self->update_reservation();
1938 sub do_reservation_return {
1940 my $request = shift;
1942 $self->log_me("do_reservation_return()");
1944 if (not ref $self->reservation) {
1945 my ($reservation, $evt) =
1946 $U->fetch_booking_reservation($self->reservation);
1947 return $self->bail_on_events($evt) if $evt;
1948 $self->reservation($reservation);
1951 $self->generate_fines(1);
1952 $self->reservation->return_time('now');
1953 $self->update_reservation();
1954 $self->reshelve_copy if $self->copy;
1956 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1957 $self->copy( $self->reservation->current_resource->catalog_item );
1961 sub booking_adjusted_due_date {
1963 my $circ = $self->circ;
1964 my $copy = $self->copy;
1966 return undef unless $self->use_booking;
1970 if( $self->due_date ) {
1972 return $self->bail_on_events($self->editor->event)
1973 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1975 $circ->due_date(cleanse_ISO8601($self->due_date));
1979 return unless $copy and $circ->due_date;
1982 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1983 if (@$booking_items) {
1984 my $booking_item = $booking_items->[0];
1985 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1987 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1988 my $shorten_circ_setting = $resource_type->elbow_room ||
1989 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1992 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1993 my $bookings = $booking_ses->request(
1994 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1995 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1997 $booking_ses->disconnect;
1999 my $dt_parser = DateTime::Format::ISO8601->new;
2000 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2002 for my $bid (@$bookings) {
2004 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2006 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2007 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2009 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2010 if ($booking_start < DateTime->now);
2013 if ($U->is_true($stop_circ_setting)) {
2014 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2016 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2017 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2020 # We set the circ duration here only to affect the logic that will
2021 # later (in a DB trigger) mangle the time part of the due date to
2022 # 11:59pm. Having any circ duration that is not a whole number of
2023 # days is enough to prevent the "correction."
2024 my $new_circ_duration = $due_date->epoch - time;
2025 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2026 $circ->duration("$new_circ_duration seconds");
2028 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2032 return $self->bail_on_events($self->editor->event)
2033 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2039 sub apply_modified_due_date {
2041 my $shift_earlier = shift;
2042 my $circ = $self->circ;
2043 my $copy = $self->copy;
2045 if( $self->due_date ) {
2047 return $self->bail_on_events($self->editor->event)
2048 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2050 $circ->due_date(cleanse_ISO8601($self->due_date));
2054 # if the due_date lands on a day when the location is closed
2055 return unless $copy and $circ->due_date;
2057 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2059 # due-date overlap should be determined by the location the item
2060 # is checked out from, not the owning or circ lib of the item
2061 my $org = $self->circ_lib;
2063 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2064 " with an item due date of ".$circ->due_date );
2066 my $dateinfo = $U->storagereq(
2067 'open-ils.storage.actor.org_unit.closed_date.overlap',
2068 $org, $circ->due_date );
2071 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2072 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2074 # XXX make the behavior more dynamic
2075 # for now, we just push the due date to after the close date
2076 if ($shift_earlier) {
2077 $circ->due_date($dateinfo->{start});
2079 $circ->due_date($dateinfo->{end});
2087 sub create_due_date {
2088 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2090 # if there is a raw time component (e.g. from postgres),
2091 # turn it into an interval that interval_to_seconds can parse
2092 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2094 # for now, use the server timezone. TODO: use workstation org timezone
2095 my $due_date = DateTime->now(time_zone => 'local');
2097 # add the circ duration
2098 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2101 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2102 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2103 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2108 # return ISO8601 time with timezone
2109 return $due_date->strftime('%FT%T%z');
2114 sub make_precat_copy {
2116 my $copy = $self->copy;
2119 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2121 $copy->editor($self->editor->requestor->id);
2122 $copy->edit_date('now');
2123 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2124 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2125 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2126 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2127 $self->update_copy();
2131 $logger->info("circulator: Creating a new precataloged ".
2132 "copy in checkout with barcode " . $self->copy_barcode);
2134 $copy = Fieldmapper::asset::copy->new;
2135 $copy->circ_lib($self->circ_lib);
2136 $copy->creator($self->editor->requestor->id);
2137 $copy->editor($self->editor->requestor->id);
2138 $copy->barcode($self->copy_barcode);
2139 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2140 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2141 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2143 $copy->dummy_title($self->dummy_title || "");
2144 $copy->dummy_author($self->dummy_author || "");
2145 $copy->dummy_isbn($self->dummy_isbn || "");
2146 $copy->circ_modifier($self->circ_modifier);
2149 # See if we need to override the circ_lib for the copy with a configured circ_lib
2150 # Setting is shortname of the org unit
2151 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2152 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2154 if($precat_circ_lib) {
2155 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2158 $self->bail_on_events($self->editor->event);
2162 $copy->circ_lib($org->id);
2166 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2168 $self->push_events($self->editor->event);
2172 # this is a little bit of a hack, but we need to
2173 # get the copy into the script runner
2174 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2178 sub checkout_noncat {
2184 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2185 my $count = $self->noncat_count || 1;
2186 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2188 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2192 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2193 $self->editor->requestor->id,
2201 $self->push_events($evt);
2212 $self->log_me("do_checkin()");
2214 return $self->bail_on_events(
2215 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2218 # the renew code and mk_env should have already found our circulation object
2219 unless( $self->circ ) {
2221 my $circs = $self->editor->search_action_circulation(
2222 { target_copy => $self->copy->id, checkin_time => undef });
2224 $self->circ($$circs[0]);
2226 # for now, just warn if there are multiple open circs on a copy
2227 $logger->warn("circulator: we have ".scalar(@$circs).
2228 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2231 # run the fine generator against this circ, if this circ is there
2232 $self->generate_fines_start if $self->circ;
2234 if( $self->checkin_check_holds_shelf() ) {
2235 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2236 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2237 $self->checkin_flesh_events;
2241 unless( $self->is_renewal ) {
2242 return $self->bail_on_events($self->editor->event)
2243 unless $self->editor->allowed('COPY_CHECKIN');
2246 $self->push_events($self->check_copy_alert());
2247 $self->push_events($self->check_checkin_copy_status());
2249 # if the circ is marked as 'claims returned', add the event to the list
2250 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2251 if ($self->circ and $self->circ->stop_fines
2252 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2254 $self->check_circ_deposit();
2256 # handle the overridable events
2257 $self->override_events unless $self->is_renewal;
2258 return if $self->bail_out;
2262 $self->editor->search_action_transit_copy(
2263 { target_copy => $self->copy->id, dest_recv_time => undef }
2269 $self->generate_fines_finish;
2270 $self->checkin_handle_circ;
2271 return if $self->bail_out;
2272 $self->checkin_changed(1);
2274 } elsif( $self->transit ) {
2275 my $hold_transit = $self->process_received_transit;
2276 $self->checkin_changed(1);
2278 if( $self->bail_out ) {
2279 $self->checkin_flesh_events;
2283 if( my $e = $self->check_checkin_copy_status() ) {
2284 # If the original copy status is special, alert the caller
2285 my $ev = $self->events;
2286 $self->events([$e]);
2287 $self->override_events;
2288 return if $self->bail_out;
2292 if( $hold_transit or
2293 $U->copy_status($self->copy->status)->id
2294 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2297 if( $hold_transit ) {
2298 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2300 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2305 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2307 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2308 $self->reshelve_copy(1);
2309 $self->cancelled_hold_transit(1);
2310 $self->notify_hold(0); # don't notify for cancelled holds
2311 return if $self->bail_out;
2315 # hold transited to correct location
2316 $self->checkin_flesh_events;
2321 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2323 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2324 " that is in-transit, but there is no transit.. repairing");
2325 $self->reshelve_copy(1);
2326 return if $self->bail_out;
2329 if( $self->is_renewal ) {
2330 $self->finish_fines_and_voiding;
2331 return if $self->bail_out;
2332 $self->push_events(OpenILS::Event->new('SUCCESS'));
2336 # ------------------------------------------------------------------------------
2337 # Circulations and transits are now closed where necessary. Now go on to see if
2338 # this copy can fulfill a hold or needs to be routed to a different location
2339 # ------------------------------------------------------------------------------
2341 my $needed_for_something = 0; # formerly "needed_for_hold"
2343 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2345 if (!$self->remote_hold) {
2346 if ($self->use_booking) {
2347 my $potential_hold = $self->hold_capture_is_possible;
2348 my $potential_reservation = $self->reservation_capture_is_possible;
2350 if ($potential_hold and $potential_reservation) {
2351 $logger->info("circulator: item could fulfill either hold or reservation");
2352 $self->push_events(new OpenILS::Event(
2353 "HOLD_RESERVATION_CONFLICT",
2354 "hold" => $potential_hold,
2355 "reservation" => $potential_reservation
2357 return if $self->bail_out;
2358 } elsif ($potential_hold) {
2359 $needed_for_something =
2360 $self->attempt_checkin_hold_capture;
2361 } elsif ($potential_reservation) {
2362 $needed_for_something =
2363 $self->attempt_checkin_reservation_capture;
2366 $needed_for_something = $self->attempt_checkin_hold_capture;
2369 return if $self->bail_out;
2371 unless($needed_for_something) {
2372 my $circ_lib = (ref $self->copy->circ_lib) ?
2373 $self->copy->circ_lib->id : $self->copy->circ_lib;
2375 if( $self->remote_hold ) {
2376 $circ_lib = $self->remote_hold->pickup_lib;
2377 $logger->warn("circulator: Copy ".$self->copy->barcode.
2378 " is on a remote hold's shelf, sending to $circ_lib");
2381 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2383 if( $circ_lib == $self->circ_lib) {
2384 # copy is where it needs to be, either for hold or reshelving
2386 $self->checkin_handle_precat();
2387 return if $self->bail_out;
2390 # copy needs to transit "home", or stick here if it's a floating copy
2392 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2393 $self->checkin_changed(1);
2394 $self->copy->circ_lib( $self->circ_lib );
2397 my $bc = $self->copy->barcode;
2398 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2399 $self->checkin_build_copy_transit($circ_lib);
2400 return if $self->bail_out;
2401 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2405 } else { # no-op checkin
2406 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2407 $self->checkin_changed(1);
2408 $self->copy->circ_lib( $self->circ_lib );
2413 if($self->claims_never_checked_out and
2414 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2416 # the item was not supposed to be checked out to the user and should now be marked as missing
2417 $self->copy->status(OILS_COPY_STATUS_MISSING);
2421 $self->reshelve_copy unless $needed_for_something;
2424 return if $self->bail_out;
2426 unless($self->checkin_changed) {
2428 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2429 my $stat = $U->copy_status($self->copy->status)->id;
2431 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2432 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2433 $self->bail_out(1); # no need to commit anything
2437 $self->push_events(OpenILS::Event->new('SUCCESS'))
2438 unless @{$self->events};
2441 $self->finish_fines_and_voiding;
2443 OpenILS::Utils::Penalty->calculate_penalties(
2444 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2446 $self->checkin_flesh_events;
2450 sub finish_fines_and_voiding {
2452 return unless $self->circ;
2454 # gather any updates to the circ after fine generation, if there was a circ
2455 $self->generate_fines_finish;
2457 return unless $self->backdate or $self->void_overdues;
2459 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2460 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2462 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2463 $self->editor, $self->circ, $self->backdate, $note);
2465 return $self->bail_on_events($evt) if $evt;
2467 # make sure the circ isn't closed if we just voided some fines
2468 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2469 return $self->bail_on_events($evt) if $evt;
2475 # if a deposit was payed for this item, push the event
2476 sub check_circ_deposit {
2478 return unless $self->circ;
2479 my $deposit = $self->editor->search_money_billing(
2481 xact => $self->circ->id,
2483 }, {idlist => 1})->[0];
2485 $self->push_events(OpenILS::Event->new(
2486 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2491 my $force = $self->force || shift;
2492 my $copy = $self->copy;
2494 my $stat = $U->copy_status($copy->status)->id;
2497 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2498 $stat != OILS_COPY_STATUS_CATALOGING and
2499 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2500 $stat != OILS_COPY_STATUS_RESHELVING )) {
2502 $copy->status( OILS_COPY_STATUS_RESHELVING );
2504 $self->checkin_changed(1);
2509 # Returns true if the item is at the current location
2510 # because it was transited there for a hold and the
2511 # hold has not been fulfilled
2512 sub checkin_check_holds_shelf {
2514 return 0 unless $self->copy;
2517 $U->copy_status($self->copy->status)->id ==
2518 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2520 # find the hold that put us on the holds shelf
2521 my $holds = $self->editor->search_action_hold_request(
2523 current_copy => $self->copy->id,
2524 capture_time => { '!=' => undef },
2525 fulfillment_time => undef,
2526 cancel_time => undef,
2531 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2532 $self->reshelve_copy(1);
2536 my $hold = $$holds[0];
2538 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2539 $hold->id. "] for copy ".$self->copy->barcode);
2541 if( $hold->pickup_lib == $self->circ_lib ) {
2542 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2546 $logger->info("circulator: hold is not for here..");
2547 $self->remote_hold($hold);
2552 sub checkin_handle_precat {
2554 my $copy = $self->copy;
2556 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2557 $copy->status(OILS_COPY_STATUS_CATALOGING);
2558 $self->update_copy();
2559 $self->checkin_changed(1);
2560 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2565 sub checkin_build_copy_transit {
2568 my $copy = $self->copy;
2569 my $transit = Fieldmapper::action::transit_copy->new;
2571 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2572 $logger->info("circulator: transiting copy to $dest");
2574 $transit->source($self->circ_lib);
2575 $transit->dest($dest);
2576 $transit->target_copy($copy->id);
2577 $transit->source_send_time('now');
2578 $transit->copy_status( $U->copy_status($copy->status)->id );
2580 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2582 return $self->bail_on_events($self->editor->event)
2583 unless $self->editor->create_action_transit_copy($transit);
2585 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2587 $self->checkin_changed(1);
2591 sub hold_capture_is_possible {
2593 my $copy = $self->copy;
2595 # we've been explicitly told not to capture any holds
2596 return 0 if $self->capture eq 'nocapture';
2598 # See if this copy can fulfill any holds
2599 my $hold = $holdcode->find_nearest_permitted_hold(
2600 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2602 return undef if ref $hold eq "HASH" and
2603 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2607 sub reservation_capture_is_possible {
2609 my $copy = $self->copy;
2611 # we've been explicitly told not to capture any holds
2612 return 0 if $self->capture eq 'nocapture';
2614 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2615 my $resv = $booking_ses->request(
2616 "open-ils.booking.reservations.could_capture",
2617 $self->editor->authtoken, $copy->barcode
2619 $booking_ses->disconnect;
2620 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2621 $self->push_events($resv);
2627 # returns true if the item was used (or may potentially be used
2628 # in subsequent calls) to capture a hold.
2629 sub attempt_checkin_hold_capture {
2631 my $copy = $self->copy;
2633 # we've been explicitly told not to capture any holds
2634 return 0 if $self->capture eq 'nocapture';
2636 # See if this copy can fulfill any holds
2637 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2638 $self->editor, $copy, $self->editor->requestor );
2641 $logger->debug("circulator: no potential permitted".
2642 "holds found for copy ".$copy->barcode);
2646 if($self->capture ne 'capture') {
2647 # see if this item is in a hold-capture-delay location
2648 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2649 if($U->is_true($location->hold_verify)) {
2650 $self->bail_on_events(
2651 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2656 $self->retarget($retarget);
2658 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2660 $hold->current_copy($copy->id);
2661 $hold->capture_time('now');
2662 $self->put_hold_on_shelf($hold)
2663 if $hold->pickup_lib == $self->circ_lib;
2665 # prevent DB errors caused by fetching
2666 # holds from storage, and updating through cstore
2667 $hold->clear_fulfillment_time;
2668 $hold->clear_fulfillment_staff;
2669 $hold->clear_fulfillment_lib;
2670 $hold->clear_expire_time;
2671 $hold->clear_cancel_time;
2672 $hold->clear_prev_check_time unless $hold->prev_check_time;
2674 $self->bail_on_events($self->editor->event)
2675 unless $self->editor->update_action_hold_request($hold);
2677 $self->checkin_changed(1);
2679 return 0 if $self->bail_out;
2681 if( $hold->pickup_lib == $self->circ_lib ) {
2683 # This hold was captured in the correct location
2684 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2685 $self->push_events(OpenILS::Event->new('SUCCESS'));
2687 #$self->do_hold_notify($hold->id);
2688 $self->notify_hold($hold->id);
2692 # Hold needs to be picked up elsewhere. Build a hold
2693 # transit and route the item.
2694 $self->checkin_build_hold_transit();
2695 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2696 return 0 if $self->bail_out;
2697 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2700 # make sure we save the copy status
2705 sub attempt_checkin_reservation_capture {
2707 my $copy = $self->copy;
2709 # we've been explicitly told not to capture any holds
2710 return 0 if $self->capture eq 'nocapture';
2712 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2713 my $evt = $booking_ses->request(
2714 "open-ils.booking.resources.capture_for_reservation",
2715 $self->editor->authtoken,
2717 1 # don't update copy - we probably have it locked
2719 $booking_ses->disconnect;
2721 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2723 "open-ils.booking.resources.capture_for_reservation " .
2724 "didn't return an event!"
2728 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2729 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2731 # not-transferable is an error event we'll pass on the user
2732 $logger->warn("reservation capture attempted against non-transferable item");
2733 $self->push_events($evt);
2735 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2736 # Re-retrieve copy as reservation capture may have changed
2737 # its status and whatnot.
2739 "circulator: booking capture win on copy " . $self->copy->id
2741 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2743 "circulator: changing copy " . $self->copy->id .
2744 "'s status from " . $self->copy->status . " to " .
2747 $self->copy->status($new_copy_status);
2750 $self->reservation($evt->{"payload"}->{"reservation"});
2752 if (exists $evt->{"payload"}->{"transit"}) {
2756 "org" => $evt->{"payload"}->{"transit"}->dest
2760 $self->checkin_changed(1);
2764 # other results are treated as "nothing to capture"
2768 sub do_hold_notify {
2769 my( $self, $holdid ) = @_;
2771 my $e = new_editor(xact => 1);
2772 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2774 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2775 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2777 $logger->info("circulator: running delayed hold notify process");
2779 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2780 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2782 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2783 hold_id => $holdid, requestor => $self->editor->requestor);
2785 $logger->debug("circulator: built hold notifier");
2787 if(!$notifier->event) {
2789 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2791 my $stat = $notifier->send_email_notify;
2792 if( $stat == '1' ) {
2793 $logger->info("circulator: hold notify succeeded for hold $holdid");
2797 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2800 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2804 sub retarget_holds {
2806 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2807 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2808 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2809 # no reason to wait for the return value
2813 sub checkin_build_hold_transit {
2816 my $copy = $self->copy;
2817 my $hold = $self->hold;
2818 my $trans = Fieldmapper::action::hold_transit_copy->new;
2820 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2822 $trans->hold($hold->id);
2823 $trans->source($self->circ_lib);
2824 $trans->dest($hold->pickup_lib);
2825 $trans->source_send_time("now");
2826 $trans->target_copy($copy->id);
2828 # when the copy gets to its destination, it will recover
2829 # this status - put it onto the holds shelf
2830 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2832 return $self->bail_on_events($self->editor->event)
2833 unless $self->editor->create_action_hold_transit_copy($trans);
2838 sub process_received_transit {
2840 my $copy = $self->copy;
2841 my $copyid = $self->copy->id;
2843 my $status_name = $U->copy_status($copy->status)->name;
2844 $logger->debug("circulator: attempting transit receive on ".
2845 "copy $copyid. Copy status is $status_name");
2847 my $transit = $self->transit;
2849 if( $transit->dest != $self->circ_lib ) {
2850 # - this item is in-transit to a different location
2852 my $tid = $transit->id;
2853 my $loc = $self->circ_lib;
2854 my $dest = $transit->dest;
2856 $logger->info("circulator: Fowarding transit on copy which is destined ".
2857 "for a different location. transit=$tid, copy=$copyid, current ".
2858 "location=$loc, destination location=$dest");
2860 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2862 # grab the associated hold object if available
2863 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2864 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2866 return $self->bail_on_events($evt);
2869 # The transit is received, set the receive time
2870 $transit->dest_recv_time('now');
2871 $self->bail_on_events($self->editor->event)
2872 unless $self->editor->update_action_transit_copy($transit);
2874 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2876 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2877 $copy->status( $transit->copy_status );
2878 $self->update_copy();
2879 return if $self->bail_out;
2883 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2885 # hold has arrived at destination, set shelf time
2886 $self->put_hold_on_shelf($hold);
2887 $self->bail_on_events($self->editor->event)
2888 unless $self->editor->update_action_hold_request($hold);
2889 return if $self->bail_out;
2891 $self->notify_hold($hold_transit->hold);
2896 OpenILS::Event->new(
2899 payload => { transit => $transit, holdtransit => $hold_transit } ));
2901 return $hold_transit;
2905 # ------------------------------------------------------------------
2906 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2907 # ------------------------------------------------------------------
2908 sub put_hold_on_shelf {
2909 my($self, $hold) = @_;
2911 $hold->shelf_time('now');
2913 my $shelf_expire = $U->ou_ancestor_setting_value(
2914 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2917 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2918 my $expire_time = DateTime->now->add(seconds => $seconds);
2919 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2927 sub generate_fines {
2929 my $reservation = shift;
2931 $self->generate_fines_start($reservation);
2932 $self->generate_fines_finish($reservation);
2937 sub generate_fines_start {
2939 my $reservation = shift;
2940 my $dt_parser = DateTime::Format::ISO8601->new;
2942 my $obj = $reservation ? $self->reservation : $self->circ;
2944 # If we have a grace period
2945 if($obj->can('grace_period')) {
2946 # Parse out the due date
2947 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
2948 # Add the grace period to the due date
2949 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
2950 # Don't generate fines on circs still in grace period
2951 return undef if ($due_date > DateTime->now);
2954 if (!exists($self->{_gen_fines_req})) {
2955 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
2957 'open-ils.storage.action.circulation.overdue.generate_fines',
2965 sub generate_fines_finish {
2967 my $reservation = shift;
2969 return undef unless $self->{_gen_fines_req};
2971 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2973 $self->{_gen_fines_req}->wait_complete;
2974 delete($self->{_gen_fines_req});
2976 # refresh the circ in case the fine generator set the stop_fines field
2977 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2978 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2983 sub checkin_handle_circ {
2985 my $circ = $self->circ;
2986 my $copy = $self->copy;
2990 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2992 # backdate the circ if necessary
2993 if($self->backdate) {
2994 my $evt = $self->checkin_handle_backdate;
2995 return $self->bail_on_events($evt) if $evt;
2998 if(!$circ->stop_fines) {
2999 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3000 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3001 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3002 $circ->stop_fines_time('now');
3003 $circ->stop_fines_time($self->backdate) if $self->backdate;
3006 # Set the checkin vars since we have the item
3007 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3009 # capture the true scan time for back-dated checkins
3010 $circ->checkin_scan_time('now');
3012 $circ->checkin_staff($self->editor->requestor->id);
3013 $circ->checkin_lib($self->circ_lib);
3014 $circ->checkin_workstation($self->editor->requestor->wsid);
3016 my $circ_lib = (ref $self->copy->circ_lib) ?
3017 $self->copy->circ_lib->id : $self->copy->circ_lib;
3018 my $stat = $U->copy_status($self->copy->status)->id;
3020 # immediately available keeps items lost or missing items from going home before being handled
3021 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3022 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3025 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3027 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3028 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3030 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3034 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3036 $self->checkin_handle_lost($circ_lib);
3040 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3045 # see if there are any fines owed on this circ. if not, close it
3046 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3047 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3049 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3051 return $self->bail_on_events($self->editor->event)
3052 unless $self->editor->update_action_circulation($circ);
3058 # ------------------------------------------------------------------
3059 # See if we need to void billings for lost checkin
3060 # ------------------------------------------------------------------
3061 sub checkin_handle_lost {
3063 my $circ_lib = shift;
3064 my $circ = $self->circ;
3066 my $max_return = $U->ou_ancestor_setting_value(
3067 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3072 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3073 $tm[5] -= 1 if $tm[5] > 0;
3074 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3076 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3077 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3079 $max_return = 0 if $today < $last_chance;
3082 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3084 my $void_lost = $U->ou_ancestor_setting_value(
3085 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3086 my $void_lost_fee = $U->ou_ancestor_setting_value(
3087 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3088 my $restore_od = $U->ou_ancestor_setting_value(
3089 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3091 $self->checkin_handle_lost_now_found(3) if $void_lost;
3092 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3093 $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
3096 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3101 sub checkin_handle_backdate {
3104 # ------------------------------------------------------------------
3105 # clean up the backdate for date comparison
3106 # XXX We are currently taking the due-time from the original due-date,
3107 # not the input. Do we need to do this? This certainly interferes with
3108 # backdating of hourly checkouts, but that is likely a very rare case.
3109 # ------------------------------------------------------------------
3110 my $bd = cleanse_ISO8601($self->backdate);
3111 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3112 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3113 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3115 $self->backdate($bd);
3120 sub check_checkin_copy_status {
3122 my $copy = $self->copy;
3124 my $status = $U->copy_status($copy->status)->id;
3127 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3128 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3129 $status == OILS_COPY_STATUS_IN_PROCESS ||
3130 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3131 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3132 $status == OILS_COPY_STATUS_CATALOGING ||
3133 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3134 $status == OILS_COPY_STATUS_RESHELVING );
3136 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3137 if( $status == OILS_COPY_STATUS_LOST );
3139 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3140 if( $status == OILS_COPY_STATUS_MISSING );
3142 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3147 # --------------------------------------------------------------------------
3148 # On checkin, we need to return as many relevant objects as we can
3149 # --------------------------------------------------------------------------
3150 sub checkin_flesh_events {
3153 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3154 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3155 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3158 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3161 if($self->hold and !$self->hold->cancel_time) {
3162 $hold = $self->hold;
3163 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3167 # if we checked in a circulation, flesh the billing summary data
3168 $self->circ->billable_transaction(
3169 $self->editor->retrieve_money_billable_transaction([
3171 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3177 # flesh some patron fields before returning
3179 $self->editor->retrieve_actor_user([
3184 au => ['card', 'billing_address', 'mailing_address']
3191 for my $evt (@{$self->events}) {
3194 $payload->{copy} = $U->unflesh_copy($self->copy);
3195 $payload->{volume} = $self->volume;
3196 $payload->{record} = $record,
3197 $payload->{circ} = $self->circ;
3198 $payload->{transit} = $self->transit;
3199 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3200 $payload->{hold} = $hold;
3201 $payload->{patron} = $self->patron;
3202 $payload->{reservation} = $self->reservation
3203 unless (not $self->reservation or $self->reservation->cancel_time);
3205 $evt->{payload} = $payload;
3210 my( $self, $msg ) = @_;
3211 my $bc = ($self->copy) ? $self->copy->barcode :
3214 my $usr = ($self->patron) ? $self->patron->id : "";
3215 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3216 ", recipient=$usr, copy=$bc");
3222 $self->log_me("do_renew()");
3224 # Make sure there is an open circ to renew that is not
3225 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3226 my $usrid = $self->patron->id if $self->patron;
3227 my $circ = $self->editor->search_action_circulation({
3228 target_copy => $self->copy->id,
3229 xact_finish => undef,
3230 checkin_time => undef,
3231 ($usrid ? (usr => $usrid) : ()),
3233 {stop_fines => undef},
3234 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3238 return $self->bail_on_events($self->editor->event) unless $circ;
3240 # A user is not allowed to renew another user's items without permission
3241 unless( $circ->usr eq $self->editor->requestor->id ) {
3242 return $self->bail_on_events($self->editor->events)
3243 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3246 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3247 if $circ->renewal_remaining < 1;
3249 # -----------------------------------------------------------------
3251 $self->parent_circ($circ->id);
3252 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3255 # Run the fine generator against the old circ
3256 $self->generate_fines_start;
3258 $self->run_renew_permit;
3261 $self->do_checkin();
3262 return if $self->bail_out;
3264 unless( $self->permit_override ) {
3266 return if $self->bail_out;
3267 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3268 $self->remove_event('ITEM_NOT_CATALOGED');
3271 $self->override_events;
3272 return if $self->bail_out;
3275 $self->do_checkout();
3280 my( $self, $evt ) = @_;
3281 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3282 $logger->debug("circulator: removing event from list: $evt");
3283 my @events = @{$self->events};
3284 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3289 my( $self, $evt ) = @_;
3290 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3291 return grep { $_->{textcode} eq $evt } @{$self->events};
3296 sub run_renew_permit {
3299 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3300 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3301 $self->editor, $self->copy, $self->editor->requestor, 1
3303 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3306 if(!$self->legacy_script_support) {
3307 my $results = $self->run_indb_circ_test;
3308 $self->push_events($self->matrix_test_result_events)
3309 unless $self->circ_test_success;
3312 my $runner = $self->script_runner;
3314 $runner->load($self->circ_permit_renew);
3315 my $result = $runner->run or
3316 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3317 if ($result->{"events"}) {
3319 map { new OpenILS::Event($_) } @{$result->{"events"}}
3322 "circulator: circ_permit_renew for user " .
3323 $self->patron->id . " returned " .
3324 scalar(@{$result->{"events"}}) . " event(s)"
3328 $self->mk_script_runner;
3331 $logger->debug("circulator: re-creating script runner to be safe");
3335 # XXX: The primary mechanism for storing circ history is now handled
3336 # by tracking real circulation objects instead of bibs in a bucket.
3337 # However, this code is disabled by default and could be useful
3338 # some day, so may as well leave it for now.
3339 sub append_reading_list {
3343 $self->is_checkout and
3349 # verify history is globally enabled and uses the bucket mechanism
3350 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3351 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3353 return undef unless $htype and $htype eq 'bucket';
3355 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3357 # verify the patron wants to retain the hisory
3358 my $setting = $e->search_actor_user_setting(
3359 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3361 unless($setting and $setting->value) {
3366 my $bkt = $e->search_container_copy_bucket(
3367 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3372 # find the next item position
3373 my $last_item = $e->search_container_copy_bucket_item(
3374 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3375 $pos = $last_item->pos + 1 if $last_item;
3378 # create the history bucket if necessary
3379 $bkt = Fieldmapper::container::copy_bucket->new;
3380 $bkt->owner($self->patron->id);
3382 $bkt->btype('circ_history');
3384 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3387 my $item = Fieldmapper::container::copy_bucket_item->new;
3389 $item->bucket($bkt->id);
3390 $item->target_copy($self->copy->id);
3393 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3400 sub make_trigger_events {
3402 return unless $self->circ;
3403 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3404 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3405 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3410 sub checkin_handle_lost_now_found {
3411 my ($self, $bill_type) = @_;
3413 # ------------------------------------------------------------------
3414 # remove charge from patron's account if lost item is returned
3415 # ------------------------------------------------------------------
3417 my $bills = $self->editor->search_money_billing(
3419 xact => $self->circ->id,
3424 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3425 for my $bill (@$bills) {
3426 if( !$U->is_true($bill->voided) ) {
3427 $logger->info("lost item returned - voiding bill ".$bill->id);
3429 $bill->void_time('now');
3430 $bill->voider($self->editor->requestor->id);
3431 my $note = ($bill->note) ? $bill->note . "\n" : '';
3432 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3434 $self->bail_on_events($self->editor->event)
3435 unless $self->editor->update_money_billing($bill);
3440 sub checkin_handle_lost_now_found_restore_od {
3443 # ------------------------------------------------------------------
3444 # restore those overdue charges voided when item was set to lost
3445 # ------------------------------------------------------------------
3447 my $ods = $self->editor->search_money_billing(
3449 xact => $self->circ->id,
3454 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3455 for my $bill (@$ods) {
3456 if( $U->is_true($bill->voided) ) {
3457 $logger->info("lost item returned - restoring overdue ".$bill->id);
3459 $bill->clear_void_time;
3460 $bill->voider($self->editor->requestor->id);
3461 my $note = ($bill->note) ? $bill->note . "\n" : '';
3462 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3464 $self->bail_on_events($self->editor->event)
3465 unless $self->editor->update_money_billing($bill);