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;
346 $circulator->editor->commit;
348 if ($circulator->generate_lost_overdue) {
349 # Generating additional overdue billings has to happen after the
350 # main commit and before the final respond() so the caller can
351 # receive the latest transaction summary.
352 my $evt = $circulator->generate_lost_overdue_fines;
353 $circulator->bail_on_events($evt) if $evt;
357 $conn->respond_complete(circ_events($circulator));
359 $circulator->script_runner->cleanup if $circulator->script_runner;
361 return undef if $circulator->bail_out;
363 $circulator->do_hold_notify($circulator->notify_hold)
364 if $circulator->notify_hold;
365 $circulator->retarget_holds if $circulator->retarget;
366 $circulator->append_reading_list;
367 $circulator->make_trigger_events;
374 my @e = @{$circ->events};
375 # if we have multiple events, SUCCESS should not be one of them;
376 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
377 return (@e == 1) ? $e[0] : \@e;
381 sub translate_legacy_args {
384 if( $$args{barcode} ) {
385 $$args{copy_barcode} = $$args{barcode};
386 delete $$args{barcode};
389 if( $$args{copyid} ) {
390 $$args{copy_id} = $$args{copyid};
391 delete $$args{copyid};
394 if( $$args{patronid} ) {
395 $$args{patron_id} = $$args{patronid};
396 delete $$args{patronid};
399 if( $$args{patron} and !ref($$args{patron}) ) {
400 $$args{patron_id} = $$args{patron};
401 delete $$args{patron};
405 if( $$args{noncat} ) {
406 $$args{is_noncat} = $$args{noncat};
407 delete $$args{noncat};
410 if( $$args{precat} ) {
411 $$args{is_precat} = $$args{request_precat} = $$args{precat};
412 delete $$args{precat};
418 # --------------------------------------------------------------------------
419 # This package actually manages all of the circulation logic
420 # --------------------------------------------------------------------------
421 package OpenILS::Application::Circ::Circulator;
422 use strict; use warnings;
423 use vars q/$AUTOLOAD/;
425 use OpenILS::Utils::Fieldmapper;
426 use OpenSRF::Utils::Cache;
427 use Digest::MD5 qw(md5_hex);
428 use DateTime::Format::ISO8601;
429 use OpenILS::Utils::PermitHold;
430 use OpenSRF::Utils qw/:datetime/;
431 use OpenSRF::Utils::SettingsClient;
432 use OpenILS::Application::Circ::Holds;
433 use OpenILS::Application::Circ::Transit;
434 use OpenSRF::Utils::Logger qw(:logger);
435 use OpenILS::Utils::CStoreEditor qw/:funcs/;
436 use OpenILS::Application::Circ::ScriptBuilder;
437 use OpenILS::Const qw/:const/;
438 use OpenILS::Utils::Penalty;
439 use OpenILS::Application::Circ::CircCommon;
442 my $holdcode = "OpenILS::Application::Circ::Holds";
443 my $transcode = "OpenILS::Application::Circ::Transit";
449 # --------------------------------------------------------------------------
450 # Add a pile of automagic getter/setter methods
451 # --------------------------------------------------------------------------
452 my @AUTOLOAD_FIELDS = qw/
499 recurring_fines_level
512 cancelled_hold_transit
519 circ_matrix_matchpoint
521 legacy_script_support
531 claims_never_checked_out
536 generate_lost_overdue
542 my $type = ref($self) or die "$self is not an object";
544 my $name = $AUTOLOAD;
547 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
548 $logger->error("circulator: $type: invalid autoload field: $name");
549 die "$type: invalid autoload field: $name\n"
554 *{"${type}::${name}"} = sub {
557 $s->{$name} = $v if defined $v;
561 return $self->$name($data);
566 my( $class, $auth, %args ) = @_;
567 $class = ref($class) || $class;
568 my $self = bless( {}, $class );
571 $self->editor(new_editor(xact => 1, authtoken => $auth));
573 unless( $self->editor->checkauth ) {
574 $self->bail_on_events($self->editor->event);
578 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
580 $self->$_($args{$_}) for keys %args;
583 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
585 # if this is a renewal, default to desk_renewal
586 $self->desk_renewal(1) unless
587 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
589 $self->capture('') unless $self->capture;
591 unless(%user_groups) {
592 my $gps = $self->editor->retrieve_all_permission_grp_tree;
593 %user_groups = map { $_->id => $_ } @$gps;
600 # --------------------------------------------------------------------------
601 # True if we should discontinue processing
602 # --------------------------------------------------------------------------
604 my( $self, $bool ) = @_;
605 if( defined $bool ) {
606 $logger->info("circulator: BAILING OUT") if $bool;
607 $self->{bail_out} = $bool;
609 return $self->{bail_out};
614 my( $self, @evts ) = @_;
617 $e->{payload} = $self->copy if
618 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
620 $logger->info("circulator: pushing event ".$e->{textcode});
621 push( @{$self->events}, $e ) unless
622 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
628 return '' if $self->skip_permit_key;
629 my $key = md5_hex( time() . rand() . "$$" );
630 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
631 return $self->permit_key($key);
634 sub check_permit_key {
636 return 1 if $self->skip_permit_key;
637 my $key = $self->permit_key;
638 return 0 unless $key;
639 my $k = "oils_permit_key_$key";
640 my $one = $self->cache_handle->get_cache($k);
641 $self->cache_handle->delete_cache($k);
642 return ($one) ? 1 : 0;
645 sub seems_like_reservation {
648 # Some words about the following method:
649 # 1) It requires the VIEW_USER permission, but that's not an
650 # issue, right, since all staff should have that?
651 # 2) It returns only one reservation at a time, even if an item can be
652 # and is currently overbooked. Hmmm....
653 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
654 my $result = $booking_ses->request(
655 "open-ils.booking.reservations.by_returnable_resource_barcode",
656 $self->editor->authtoken,
659 $booking_ses->disconnect;
661 return $self->bail_on_events($result) if defined $U->event_code($result);
664 $self->reservation(shift @$result);
672 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
673 sub save_trimmed_copy {
674 my ($self, $copy) = @_;
677 $self->volume($copy->call_number);
678 $self->title($self->volume->record);
679 $self->copy->call_number($self->volume->id);
680 $self->volume->record($self->title->id);
681 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
682 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
683 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
684 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
690 my $e = $self->editor;
692 # --------------------------------------------------------------------------
693 # Grab the fleshed copy
694 # --------------------------------------------------------------------------
695 unless($self->is_noncat) {
698 $copy = $e->retrieve_asset_copy(
699 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
701 } elsif( $self->copy_barcode ) {
703 $copy = $e->search_asset_copy(
704 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
705 } elsif( $self->reservation ) {
706 my $res = $e->json_query(
708 "select" => {"acp" => ["id"]},
713 "field" => "barcode",
717 "field" => "current_resource"
725 "id" => (ref $self->reservation) ?
726 $self->reservation->id : $self->reservation
731 if (ref $res eq "ARRAY" and scalar @$res) {
732 $logger->info("circulator: mapped reservation " .
733 $self->reservation . " to copy " . $res->[0]->{"id"});
734 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
739 $self->save_trimmed_copy($copy);
741 # We can't renew if there is no copy
742 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
743 if $self->is_renewal;
748 # --------------------------------------------------------------------------
750 # --------------------------------------------------------------------------
754 flesh_fields => {au => [ qw/ card / ]}
757 if( $self->patron_id ) {
758 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
759 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
761 } elsif( $self->patron_barcode ) {
763 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
764 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
765 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
767 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
768 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
771 if( my $copy = $self->copy ) {
774 $flesh->{flesh_fields}->{circ} = ['usr'];
776 my $circ = $e->search_action_circulation([
777 {target_copy => $copy->id, checkin_time => undef}, $flesh
781 $patron = $circ->usr;
782 $circ->usr($patron->id); # de-flesh for consistency
788 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
789 unless $self->patron($patron) or $self->is_checkin;
791 unless($self->is_checkin) {
793 # Check for inactivity and patron reg. expiration
795 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
796 unless $U->is_true($patron->active);
798 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
799 unless $U->is_true($patron->card->active);
801 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
802 cleanse_ISO8601($patron->expire_date));
804 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
805 if( CORE::time > $expire->epoch ) ;
809 # --------------------------------------------------------------------------
810 # This builds the script runner environment and fetches most of the
812 # --------------------------------------------------------------------------
813 sub mk_script_runner {
819 qw/copy copy_barcode copy_id patron
820 patron_id patron_barcode volume title editor/;
822 # Translate our objects into the ScriptBuilder args hash
823 $$args{$_} = $self->$_() for @fields;
825 $args->{ignore_user_status} = 1 if $self->is_checkin;
826 $$args{fetch_patron_by_circ_copy} = 1;
827 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
829 if( my $pco = $self->pending_checkouts ) {
830 $logger->info("circulator: we were given a pending checkouts number of $pco");
831 $$args{patronItemsOut} = $pco;
834 # This fetches most of the objects we need
835 $self->script_runner(
836 OpenILS::Application::Circ::ScriptBuilder->build($args));
838 # Now we translate the ScriptBuilder objects back into self
839 $self->$_($$args{$_}) for @fields;
841 my @evts = @{$args->{_events}} if $args->{_events};
843 $logger->debug("circulator: script builder returned events: @evts") if @evts;
847 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
848 if(!$self->is_noncat and
850 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
854 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
855 return $self->bail_on_events(@e);
860 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
861 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
862 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
863 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
867 # We can't renew if there is no copy
868 return $self->bail_on_events(@evts) if
869 $self->is_renewal and !$self->copy;
871 # Set some circ-specific flags in the script environment
872 my $evt = "environment";
873 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
875 if( $self->is_noncat ) {
876 $self->script_runner->insert("$evt.isNonCat", 1);
877 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
880 if( $self->is_precat ) {
881 $self->script_runner->insert("environment.isPrecat", 1, 1);
884 $self->script_runner->add_path( $_ ) for @$script_libs;
889 # --------------------------------------------------------------------------
890 # Does the circ permit work
891 # --------------------------------------------------------------------------
895 $self->log_me("do_permit()");
897 unless( $self->editor->requestor->id == $self->patron->id ) {
898 return $self->bail_on_events($self->editor->event)
899 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
902 $self->check_captured_holds();
903 $self->do_copy_checks();
904 return if $self->bail_out;
905 $self->run_patron_permit_scripts();
906 $self->run_copy_permit_scripts()
907 unless $self->is_precat or $self->is_noncat;
908 $self->check_item_deposit_events();
909 $self->override_events();
910 return if $self->bail_out;
912 if($self->is_precat and not $self->request_precat) {
915 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
916 return $self->bail_out(1) unless $self->is_renewal;
920 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
923 sub check_item_deposit_events {
925 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
926 if $self->is_deposit and not $self->is_deposit_exempt;
927 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
928 if $self->is_rental and not $self->is_rental_exempt;
931 # returns true if the user is not required to pay deposits
932 sub is_deposit_exempt {
934 my $pid = (ref $self->patron->profile) ?
935 $self->patron->profile->id : $self->patron->profile;
936 my $groups = $U->ou_ancestor_setting_value(
937 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
938 for my $grp (@$groups) {
939 return 1 if $self->is_group_descendant($grp, $pid);
944 # returns true if the user is not required to pay rental fees
945 sub is_rental_exempt {
947 my $pid = (ref $self->patron->profile) ?
948 $self->patron->profile->id : $self->patron->profile;
949 my $groups = $U->ou_ancestor_setting_value(
950 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
951 for my $grp (@$groups) {
952 return 1 if $self->is_group_descendant($grp, $pid);
957 sub is_group_descendant {
958 my($self, $p_id, $c_id) = @_;
959 return 0 unless defined $p_id and defined $c_id;
960 return 1 if $c_id == $p_id;
961 while(my $grp = $user_groups{$c_id}) {
962 $c_id = $grp->parent;
963 return 0 unless defined $c_id;
964 return 1 if $c_id == $p_id;
969 sub check_captured_holds {
971 my $copy = $self->copy;
972 my $patron = $self->patron;
974 return undef unless $copy;
976 my $s = $U->copy_status($copy->status)->id;
977 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
978 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
980 # Item is on the holds shelf, make sure it's going to the right person
981 my $holds = $self->editor->search_action_hold_request(
984 current_copy => $copy->id ,
985 capture_time => { '!=' => undef },
986 cancel_time => undef,
987 fulfillment_time => undef
993 if( $holds and $$holds[0] ) {
994 return undef if $$holds[0]->usr == $patron->id;
997 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
999 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1003 sub do_copy_checks {
1005 my $copy = $self->copy;
1006 return unless $copy;
1008 my $stat = $U->copy_status($copy->status)->id;
1010 # We cannot check out a copy if it is in-transit
1011 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1012 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1015 $self->handle_claims_returned();
1016 return if $self->bail_out;
1018 # no claims returned circ was found, check if there is any open circ
1019 unless( $self->is_renewal ) {
1021 my $circs = $self->editor->search_action_circulation(
1022 { target_copy => $copy->id, checkin_time => undef }
1025 if(my $old_circ = $circs->[0]) { # an open circ was found
1027 my $payload = {copy => $copy};
1029 if($old_circ->usr == $self->patron->id) {
1031 $payload->{old_circ} = $old_circ;
1033 # If there is an open circulation on the checkout item and an auto-renew
1034 # interval is defined, inform the caller that they should go
1035 # ahead and renew the item instead of warning about open circulations.
1037 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1039 'circ.checkout_auto_renew_age',
1043 if($auto_renew_intvl) {
1044 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1045 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1047 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1048 $payload->{auto_renew} = 1;
1053 return $self->bail_on_events(
1054 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1060 my $LEGACY_CIRC_EVENT_MAP = {
1061 'no_item' => 'ITEM_NOT_CATALOGED',
1062 'actor.usr.barred' => 'PATRON_BARRED',
1063 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1064 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1065 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1066 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1067 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1068 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1069 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1070 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1074 # ---------------------------------------------------------------------
1075 # This pushes any patron-related events into the list but does not
1076 # set bail_out for any events
1077 # ---------------------------------------------------------------------
1078 sub run_patron_permit_scripts {
1080 my $runner = $self->script_runner;
1081 my $patronid = $self->patron->id;
1085 if(!$self->legacy_script_support) {
1087 my $results = $self->run_indb_circ_test;
1088 unless($self->circ_test_success) {
1089 # no_item result is OK during noncat checkout
1090 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1091 push @allevents, $self->matrix_test_result_events;
1097 # ---------------------------------------------------------------------
1098 # # Now run the patron permit script
1099 # ---------------------------------------------------------------------
1100 $runner->load($self->circ_permit_patron);
1101 my $result = $runner->run or
1102 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1104 my $patron_events = $result->{events};
1106 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1107 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1108 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1109 $penalties = $penalties->{fatal_penalties};
1111 for my $pen (@$penalties) {
1112 my $event = OpenILS::Event->new($pen->name);
1113 $event->{desc} = $pen->label;
1114 push(@allevents, $event);
1117 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1121 $_->{payload} = $self->copy if
1122 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1125 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1127 $self->push_events(@allevents);
1130 sub matrix_test_result_codes {
1132 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1135 sub matrix_test_result_events {
1138 my $event = new OpenILS::Event(
1139 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1141 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1143 } (@{$self->matrix_test_result});
1146 sub run_indb_circ_test {
1148 return $self->matrix_test_result if $self->matrix_test_result;
1150 my $dbfunc = ($self->is_renewal) ?
1151 'action.item_user_renew_test' : 'action.item_user_circ_test';
1153 if( $self->is_precat && $self->request_precat) {
1154 $self->make_precat_copy;
1155 return if $self->bail_out;
1158 my $results = $self->editor->json_query(
1162 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1168 $self->circ_test_success($U->is_true($results->[0]->{success}));
1170 if(my $mp = $results->[0]->{matchpoint}) {
1171 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1172 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1173 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1174 if($results->[0]->{renewals}) {
1175 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1177 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1178 if($results->[0]->{grace_period}) {
1179 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1181 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1182 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1185 return $self->matrix_test_result($results);
1188 # ---------------------------------------------------------------------
1189 # given a use and copy, this will calculate the circulation policy
1190 # parameters. Only works with in-db circ.
1191 # ---------------------------------------------------------------------
1195 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1197 $self->run_indb_circ_test;
1200 circ_test_success => $self->circ_test_success,
1201 failure_events => [],
1202 failure_codes => [],
1203 matchpoint => $self->circ_matrix_matchpoint
1206 unless($self->circ_test_success) {
1207 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1208 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1211 if($self->circ_matrix_matchpoint) {
1212 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1213 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1214 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1215 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1217 my $policy = $self->get_circ_policy(
1218 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1220 $$results{$_} = $$policy{$_} for keys %$policy;
1226 # ---------------------------------------------------------------------
1227 # Loads the circ policy info for duration, recurring fine, and max
1228 # fine based on the current copy
1229 # ---------------------------------------------------------------------
1230 sub get_circ_policy {
1231 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1234 duration_rule => $duration_rule->name,
1235 recurring_fine_rule => $recurring_fine_rule->name,
1236 max_fine_rule => $max_fine_rule->name,
1237 max_fine => $self->get_max_fine_amount($max_fine_rule),
1238 fine_interval => $recurring_fine_rule->recurrence_interval,
1239 renewal_remaining => $duration_rule->max_renewals,
1240 grace_period => $recurring_fine_rule->grace_period
1243 if($hard_due_date) {
1244 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1245 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1248 $policy->{duration_date_ceiling} = undef;
1249 $policy->{duration_date_ceiling_force} = undef;
1252 $policy->{duration} = $duration_rule->shrt
1253 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1254 $policy->{duration} = $duration_rule->normal
1255 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1256 $policy->{duration} = $duration_rule->extended
1257 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1259 $policy->{recurring_fine} = $recurring_fine_rule->low
1260 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1261 $policy->{recurring_fine} = $recurring_fine_rule->normal
1262 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1263 $policy->{recurring_fine} = $recurring_fine_rule->high
1264 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1269 sub get_max_fine_amount {
1271 my $max_fine_rule = shift;
1272 my $max_amount = $max_fine_rule->amount;
1274 # if is_percent is true then the max->amount is
1275 # use as a percentage of the copy price
1276 if ($U->is_true($max_fine_rule->is_percent)) {
1277 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1278 $max_amount = $price * $max_fine_rule->amount / 100;
1280 $U->ou_ancestor_setting_value(
1282 'circ.max_fine.cap_at_price',
1286 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1287 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1295 sub run_copy_permit_scripts {
1297 my $copy = $self->copy || return;
1298 my $runner = $self->script_runner;
1302 if(!$self->legacy_script_support) {
1303 my $results = $self->run_indb_circ_test;
1304 push @allevents, $self->matrix_test_result_events
1305 unless $self->circ_test_success;
1308 # ---------------------------------------------------------------------
1309 # Capture all of the copy permit events
1310 # ---------------------------------------------------------------------
1311 $runner->load($self->circ_permit_copy);
1312 my $result = $runner->run or
1313 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1314 my $copy_events = $result->{events};
1316 # ---------------------------------------------------------------------
1317 # Now collect all of the events together
1318 # ---------------------------------------------------------------------
1319 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1322 # See if this copy has an alert message
1323 my $ae = $self->check_copy_alert();
1324 push( @allevents, $ae ) if $ae;
1326 # uniquify the events
1327 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1328 @allevents = values %hash;
1330 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1332 $self->push_events(@allevents);
1336 sub check_copy_alert {
1338 return undef if $self->is_renewal;
1339 return OpenILS::Event->new(
1340 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1341 if $self->copy and $self->copy->alert_message;
1347 # --------------------------------------------------------------------------
1348 # If the call is overriding and has permissions to override every collected
1349 # event, the are cleared. Any event that the caller does not have
1350 # permission to override, will be left in the event list and bail_out will
1352 # XXX We need code in here to cancel any holds/transits on copies
1353 # that are being force-checked out
1354 # --------------------------------------------------------------------------
1355 sub override_events {
1357 my @events = @{$self->events};
1358 return unless @events;
1360 if(!$self->override) {
1361 return $self->bail_out(1)
1362 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1367 for my $e (@events) {
1368 my $tc = $e->{textcode};
1369 next if $tc eq 'SUCCESS';
1370 my $ov = "$tc.override";
1371 $logger->info("circulator: attempting to override event: $ov");
1373 return $self->bail_on_events($self->editor->event)
1374 unless( $self->editor->allowed($ov) );
1379 # --------------------------------------------------------------------------
1380 # If there is an open claimsreturn circ on the requested copy, close the
1381 # circ if overriding, otherwise bail out
1382 # --------------------------------------------------------------------------
1383 sub handle_claims_returned {
1385 my $copy = $self->copy;
1387 my $CR = $self->editor->search_action_circulation(
1389 target_copy => $copy->id,
1390 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1391 checkin_time => undef,
1395 return unless ($CR = $CR->[0]);
1399 # - If the caller has set the override flag, we will check the item in
1400 if($self->override) {
1402 $CR->checkin_time('now');
1403 $CR->checkin_scan_time('now');
1404 $CR->checkin_lib($self->circ_lib);
1405 $CR->checkin_workstation($self->editor->requestor->wsid);
1406 $CR->checkin_staff($self->editor->requestor->id);
1408 $evt = $self->editor->event
1409 unless $self->editor->update_action_circulation($CR);
1412 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1415 $self->bail_on_events($evt) if $evt;
1420 # --------------------------------------------------------------------------
1421 # This performs the checkout
1422 # --------------------------------------------------------------------------
1426 $self->log_me("do_checkout()");
1428 # make sure perms are good if this isn't a renewal
1429 unless( $self->is_renewal ) {
1430 return $self->bail_on_events($self->editor->event)
1431 unless( $self->editor->allowed('COPY_CHECKOUT') );
1434 # verify the permit key
1435 unless( $self->check_permit_key ) {
1436 if( $self->permit_override ) {
1437 return $self->bail_on_events($self->editor->event)
1438 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1440 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1444 # if this is a non-cataloged circ, build the circ and finish
1445 if( $self->is_noncat ) {
1446 $self->checkout_noncat;
1448 OpenILS::Event->new('SUCCESS',
1449 payload => { noncat_circ => $self->circ }));
1453 if( $self->is_precat ) {
1454 $self->make_precat_copy;
1455 return if $self->bail_out;
1457 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1458 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1461 $self->do_copy_checks;
1462 return if $self->bail_out;
1464 $self->run_checkout_scripts();
1465 return if $self->bail_out;
1467 $self->build_checkout_circ_object();
1468 return if $self->bail_out;
1470 my $modify_to_start = $self->booking_adjusted_due_date();
1471 return if $self->bail_out;
1473 $self->apply_modified_due_date($modify_to_start);
1474 return if $self->bail_out;
1476 return $self->bail_on_events($self->editor->event)
1477 unless $self->editor->create_action_circulation($self->circ);
1479 # refresh the circ to force local time zone for now
1480 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1482 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1484 return if $self->bail_out;
1486 $self->apply_deposit_fee();
1487 return if $self->bail_out;
1489 $self->handle_checkout_holds();
1490 return if $self->bail_out;
1492 # ------------------------------------------------------------------------------
1493 # Update the patron penalty info in the DB. Run it for permit-overrides
1494 # since the penalties are not updated during the permit phase
1495 # ------------------------------------------------------------------------------
1496 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1498 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1501 if($self->is_renewal) {
1502 # flesh the billing summary for the checked-in circ
1503 $pcirc = $self->editor->retrieve_action_circulation([
1505 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1510 OpenILS::Event->new('SUCCESS',
1512 copy => $U->unflesh_copy($self->copy),
1513 volume => $self->volume,
1514 circ => $self->circ,
1516 holds_fulfilled => $self->fulfilled_holds,
1517 deposit_billing => $self->deposit_billing,
1518 rental_billing => $self->rental_billing,
1519 parent_circ => $pcirc,
1520 patron => ($self->return_patron) ? $self->patron : undef,
1521 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1527 sub apply_deposit_fee {
1529 my $copy = $self->copy;
1531 ($self->is_deposit and not $self->is_deposit_exempt) or
1532 ($self->is_rental and not $self->is_rental_exempt);
1534 return if $self->is_deposit and $self->skip_deposit_fee;
1535 return if $self->is_rental and $self->skip_rental_fee;
1537 my $bill = Fieldmapper::money::billing->new;
1538 my $amount = $copy->deposit_amount;
1542 if($self->is_deposit) {
1543 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1545 $self->deposit_billing($bill);
1547 $billing_type = OILS_BILLING_TYPE_RENTAL;
1549 $self->rental_billing($bill);
1552 $bill->xact($self->circ->id);
1553 $bill->amount($amount);
1554 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1555 $bill->billing_type($billing_type);
1556 $bill->btype($btype);
1557 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1559 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1564 my $copy = $self->copy;
1566 my $stat = $copy->status if ref $copy->status;
1567 my $loc = $copy->location if ref $copy->location;
1568 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1570 $copy->status($stat->id) if $stat;
1571 $copy->location($loc->id) if $loc;
1572 $copy->circ_lib($circ_lib->id) if $circ_lib;
1573 $copy->editor($self->editor->requestor->id);
1574 $copy->edit_date('now');
1575 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1577 return $self->bail_on_events($self->editor->event)
1578 unless $self->editor->update_asset_copy($self->copy);
1580 $copy->status($U->copy_status($copy->status));
1581 $copy->location($loc) if $loc;
1582 $copy->circ_lib($circ_lib) if $circ_lib;
1585 sub update_reservation {
1587 my $reservation = $self->reservation;
1589 my $usr = $reservation->usr;
1590 my $target_rt = $reservation->target_resource_type;
1591 my $target_r = $reservation->target_resource;
1592 my $current_r = $reservation->current_resource;
1594 $reservation->usr($usr->id) if ref $usr;
1595 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1596 $reservation->target_resource($target_r->id) if ref $target_r;
1597 $reservation->current_resource($current_r->id) if ref $current_r;
1599 return $self->bail_on_events($self->editor->event)
1600 unless $self->editor->update_booking_reservation($self->reservation);
1603 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1604 $self->reservation($reservation);
1608 sub bail_on_events {
1609 my( $self, @evts ) = @_;
1610 $self->push_events(@evts);
1615 # ------------------------------------------------------------------------------
1616 # When an item is checked out, see if we can fulfill a hold for this patron
1617 # ------------------------------------------------------------------------------
1618 sub handle_checkout_holds {
1620 my $copy = $self->copy;
1621 my $patron = $self->patron;
1623 my $e = $self->editor;
1624 $self->fulfilled_holds([]);
1626 # pre/non-cats can't fulfill a hold
1627 return if $self->is_precat or $self->is_noncat;
1629 my $hold = $e->search_action_hold_request({
1630 current_copy => $copy->id ,
1631 cancel_time => undef,
1632 fulfillment_time => undef,
1634 {expire_time => undef},
1635 {expire_time => {'>' => 'now'}}
1639 if($hold and $hold->usr != $patron->id) {
1640 # reset the hold since the copy is now checked out
1642 $logger->info("circulator: un-targeting hold ".$hold->id.
1643 " because copy ".$copy->id." is getting checked out");
1645 $hold->clear_prev_check_time;
1646 $hold->clear_current_copy;
1647 $hold->clear_capture_time;
1649 return $self->bail_on_event($e->event)
1650 unless $e->update_action_hold_request($hold);
1656 $hold = $self->find_related_user_hold($copy, $patron) or return;
1657 $logger->info("circulator: found related hold to fulfill in checkout");
1660 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1662 # if the hold was never officially captured, capture it.
1663 $hold->current_copy($copy->id);
1664 $hold->capture_time('now') unless $hold->capture_time;
1665 $hold->fulfillment_time('now');
1666 $hold->fulfillment_staff($e->requestor->id);
1667 $hold->fulfillment_lib($self->circ_lib);
1669 return $self->bail_on_events($e->event)
1670 unless $e->update_action_hold_request($hold);
1672 $holdcode->delete_hold_copy_maps($e, $hold->id);
1673 return $self->fulfilled_holds([$hold->id]);
1677 # ------------------------------------------------------------------------------
1678 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1679 # the patron directly targets the checked out item, see if there is another hold
1680 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1681 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1682 # ------------------------------------------------------------------------------
1683 sub find_related_user_hold {
1684 my($self, $copy, $patron) = @_;
1685 my $e = $self->editor;
1687 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1689 return undef unless $U->ou_ancestor_setting_value(
1690 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1692 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1694 select => {ahr => ['id']},
1699 fkey => 'current_copy',
1700 type => 'left' # there may be no current_copy
1707 fulfillment_time => undef,
1708 cancel_time => undef,
1710 {expire_time => undef},
1711 {expire_time => {'>' => 'now'}}
1718 target => $self->volume->id
1724 target => $self->title->id
1730 {id => undef}, # left-join copy may be nonexistent
1731 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1735 order_by => {ahr => {request_time => {direction => 'asc'}}},
1739 my $hold_info = $e->json_query($args)->[0];
1740 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1745 sub run_checkout_scripts {
1750 my $runner = $self->script_runner;
1759 my $hard_due_date_name;
1761 if(!$self->legacy_script_support) {
1762 $self->run_indb_circ_test();
1763 $duration = $self->circ_matrix_matchpoint->duration_rule;
1764 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1765 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1766 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1770 $runner->load($self->circ_duration);
1772 my $result = $runner->run or
1773 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1775 $duration_name = $result->{durationRule};
1776 $recurring_name = $result->{recurringFinesRule};
1777 $max_fine_name = $result->{maxFine};
1778 $hard_due_date_name = $result->{hardDueDate};
1781 $duration_name = $duration->name if $duration;
1782 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1785 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1786 return $self->bail_on_events($evt) if ($evt && !$nobail);
1788 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1789 return $self->bail_on_events($evt) if ($evt && !$nobail);
1791 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1792 return $self->bail_on_events($evt) if ($evt && !$nobail);
1794 if($hard_due_date_name) {
1795 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1796 return $self->bail_on_events($evt) if ($evt && !$nobail);
1802 # The item circulates with an unlimited duration
1806 $hard_due_date = undef;
1809 $self->duration_rule($duration);
1810 $self->recurring_fines_rule($recurring);
1811 $self->max_fine_rule($max_fine);
1812 $self->hard_due_date($hard_due_date);
1816 sub build_checkout_circ_object {
1819 my $circ = Fieldmapper::action::circulation->new;
1820 my $duration = $self->duration_rule;
1821 my $max = $self->max_fine_rule;
1822 my $recurring = $self->recurring_fines_rule;
1823 my $hard_due_date = $self->hard_due_date;
1824 my $copy = $self->copy;
1825 my $patron = $self->patron;
1826 my $duration_date_ceiling;
1827 my $duration_date_ceiling_force;
1831 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1832 $duration_date_ceiling = $policy->{duration_date_ceiling};
1833 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1835 my $dname = $duration->name;
1836 my $mname = $max->name;
1837 my $rname = $recurring->name;
1839 if($hard_due_date) {
1840 $hdname = $hard_due_date->name;
1843 $logger->debug("circulator: building circulation ".
1844 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1846 $circ->duration($policy->{duration});
1847 $circ->recurring_fine($policy->{recurring_fine});
1848 $circ->duration_rule($duration->name);
1849 $circ->recurring_fine_rule($recurring->name);
1850 $circ->max_fine_rule($max->name);
1851 $circ->max_fine($policy->{max_fine});
1852 $circ->fine_interval($recurring->recurrence_interval);
1853 $circ->renewal_remaining($duration->max_renewals);
1854 $circ->grace_period($policy->{grace_period});
1858 $logger->info("circulator: copy found with an unlimited circ duration");
1859 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1860 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1861 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1862 $circ->renewal_remaining(0);
1863 $circ->grace_period(0);
1866 $circ->target_copy( $copy->id );
1867 $circ->usr( $patron->id );
1868 $circ->circ_lib( $self->circ_lib );
1869 $circ->workstation($self->editor->requestor->wsid)
1870 if defined $self->editor->requestor->wsid;
1872 # renewals maintain a link to the parent circulation
1873 $circ->parent_circ($self->parent_circ);
1875 if( $self->is_renewal ) {
1876 $circ->opac_renewal('t') if $self->opac_renewal;
1877 $circ->phone_renewal('t') if $self->phone_renewal;
1878 $circ->desk_renewal('t') if $self->desk_renewal;
1879 $circ->renewal_remaining($self->renewal_remaining);
1880 $circ->circ_staff($self->editor->requestor->id);
1884 # if the user provided an overiding checkout time,
1885 # (e.g. the checkout really happened several hours ago), then
1886 # we apply that here. Does this need a perm??
1887 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1888 if $self->checkout_time;
1890 # if a patron is renewing, 'requestor' will be the patron
1891 $circ->circ_staff($self->editor->requestor->id);
1892 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1897 sub do_reservation_pickup {
1900 $self->log_me("do_reservation_pickup()");
1902 $self->reservation->pickup_time('now');
1905 $self->reservation->current_resource &&
1906 $U->is_true($self->reservation->target_resource_type->catalog_item)
1908 # We used to try to set $self->copy and $self->patron here,
1909 # but that should already be done.
1911 $self->run_checkout_scripts(1);
1913 my $duration = $self->duration_rule;
1914 my $max = $self->max_fine_rule;
1915 my $recurring = $self->recurring_fines_rule;
1917 if ($duration && $max && $recurring) {
1918 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1920 my $dname = $duration->name;
1921 my $mname = $max->name;
1922 my $rname = $recurring->name;
1924 $logger->debug("circulator: updating reservation ".
1925 "with duration=$dname, maxfine=$mname, recurring=$rname");
1927 $self->reservation->fine_amount($policy->{recurring_fine});
1928 $self->reservation->max_fine($policy->{max_fine});
1929 $self->reservation->fine_interval($recurring->recurrence_interval);
1932 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1933 $self->update_copy();
1936 $self->reservation->fine_amount(
1937 $self->reservation->target_resource_type->fine_amount
1939 $self->reservation->max_fine(
1940 $self->reservation->target_resource_type->max_fine
1942 $self->reservation->fine_interval(
1943 $self->reservation->target_resource_type->fine_interval
1947 $self->update_reservation();
1950 sub do_reservation_return {
1952 my $request = shift;
1954 $self->log_me("do_reservation_return()");
1956 if (not ref $self->reservation) {
1957 my ($reservation, $evt) =
1958 $U->fetch_booking_reservation($self->reservation);
1959 return $self->bail_on_events($evt) if $evt;
1960 $self->reservation($reservation);
1963 $self->generate_fines(1);
1964 $self->reservation->return_time('now');
1965 $self->update_reservation();
1966 $self->reshelve_copy if $self->copy;
1968 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1969 $self->copy( $self->reservation->current_resource->catalog_item );
1973 sub booking_adjusted_due_date {
1975 my $circ = $self->circ;
1976 my $copy = $self->copy;
1978 return undef unless $self->use_booking;
1982 if( $self->due_date ) {
1984 return $self->bail_on_events($self->editor->event)
1985 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1987 $circ->due_date(cleanse_ISO8601($self->due_date));
1991 return unless $copy and $circ->due_date;
1994 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1995 if (@$booking_items) {
1996 my $booking_item = $booking_items->[0];
1997 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1999 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2000 my $shorten_circ_setting = $resource_type->elbow_room ||
2001 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2004 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2005 my $bookings = $booking_ses->request(
2006 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2007 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
2009 $booking_ses->disconnect;
2011 my $dt_parser = DateTime::Format::ISO8601->new;
2012 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2014 for my $bid (@$bookings) {
2016 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2018 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2019 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2021 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2022 if ($booking_start < DateTime->now);
2025 if ($U->is_true($stop_circ_setting)) {
2026 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2028 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2029 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2032 # We set the circ duration here only to affect the logic that will
2033 # later (in a DB trigger) mangle the time part of the due date to
2034 # 11:59pm. Having any circ duration that is not a whole number of
2035 # days is enough to prevent the "correction."
2036 my $new_circ_duration = $due_date->epoch - time;
2037 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2038 $circ->duration("$new_circ_duration seconds");
2040 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2044 return $self->bail_on_events($self->editor->event)
2045 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2051 sub apply_modified_due_date {
2053 my $shift_earlier = shift;
2054 my $circ = $self->circ;
2055 my $copy = $self->copy;
2057 if( $self->due_date ) {
2059 return $self->bail_on_events($self->editor->event)
2060 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2062 $circ->due_date(cleanse_ISO8601($self->due_date));
2066 # if the due_date lands on a day when the location is closed
2067 return unless $copy and $circ->due_date;
2069 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2071 # due-date overlap should be determined by the location the item
2072 # is checked out from, not the owning or circ lib of the item
2073 my $org = $self->circ_lib;
2075 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2076 " with an item due date of ".$circ->due_date );
2078 my $dateinfo = $U->storagereq(
2079 'open-ils.storage.actor.org_unit.closed_date.overlap',
2080 $org, $circ->due_date );
2083 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2084 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2086 # XXX make the behavior more dynamic
2087 # for now, we just push the due date to after the close date
2088 if ($shift_earlier) {
2089 $circ->due_date($dateinfo->{start});
2091 $circ->due_date($dateinfo->{end});
2099 sub create_due_date {
2100 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2102 # if there is a raw time component (e.g. from postgres),
2103 # turn it into an interval that interval_to_seconds can parse
2104 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2106 # for now, use the server timezone. TODO: use workstation org timezone
2107 my $due_date = DateTime->now(time_zone => 'local');
2109 # add the circ duration
2110 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2113 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2114 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2115 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2120 # return ISO8601 time with timezone
2121 return $due_date->strftime('%FT%T%z');
2126 sub make_precat_copy {
2128 my $copy = $self->copy;
2131 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2133 $copy->editor($self->editor->requestor->id);
2134 $copy->edit_date('now');
2135 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2136 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2137 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2138 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2139 $self->update_copy();
2143 $logger->info("circulator: Creating a new precataloged ".
2144 "copy in checkout with barcode " . $self->copy_barcode);
2146 $copy = Fieldmapper::asset::copy->new;
2147 $copy->circ_lib($self->circ_lib);
2148 $copy->creator($self->editor->requestor->id);
2149 $copy->editor($self->editor->requestor->id);
2150 $copy->barcode($self->copy_barcode);
2151 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2152 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2153 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2155 $copy->dummy_title($self->dummy_title || "");
2156 $copy->dummy_author($self->dummy_author || "");
2157 $copy->dummy_isbn($self->dummy_isbn || "");
2158 $copy->circ_modifier($self->circ_modifier);
2161 # See if we need to override the circ_lib for the copy with a configured circ_lib
2162 # Setting is shortname of the org unit
2163 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2164 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2166 if($precat_circ_lib) {
2167 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2170 $self->bail_on_events($self->editor->event);
2174 $copy->circ_lib($org->id);
2178 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2180 $self->push_events($self->editor->event);
2184 # this is a little bit of a hack, but we need to
2185 # get the copy into the script runner
2186 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2190 sub checkout_noncat {
2196 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2197 my $count = $self->noncat_count || 1;
2198 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2200 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2204 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2205 $self->editor->requestor->id,
2213 $self->push_events($evt);
2221 # If a copy goes into transit and is then checked in before the transit checkin
2222 # interval has expired, push an event onto the overridable events list.
2223 sub check_transit_checkin_interval {
2226 # only concerned with in-transit items
2227 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2229 # no interval, no problem
2230 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2231 return unless $interval;
2233 # capture the transit so we don't have to fetch it again later during checkin
2235 $self->editor->search_action_transit_copy(
2236 {target_copy => $self->copy->id, dest_recv_time => undef}
2240 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2241 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2242 my $horizon = $t_start->add(seconds => $seconds);
2244 # See if we are still within the transit checkin forbidden range
2245 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2246 if $horizon > DateTime->now;
2252 $self->log_me("do_checkin()");
2254 return $self->bail_on_events(
2255 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2258 $self->check_transit_checkin_interval;
2260 # the renew code and mk_env should have already found our circulation object
2261 unless( $self->circ ) {
2263 my $circs = $self->editor->search_action_circulation(
2264 { target_copy => $self->copy->id, checkin_time => undef });
2266 $self->circ($$circs[0]);
2268 # for now, just warn if there are multiple open circs on a copy
2269 $logger->warn("circulator: we have ".scalar(@$circs).
2270 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2273 # run the fine generator against this circ, if this circ is there
2274 $self->generate_fines_start if $self->circ;
2276 if( $self->checkin_check_holds_shelf() ) {
2277 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2278 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2279 $self->checkin_flesh_events;
2283 unless( $self->is_renewal ) {
2284 return $self->bail_on_events($self->editor->event)
2285 unless $self->editor->allowed('COPY_CHECKIN');
2288 $self->push_events($self->check_copy_alert());
2289 $self->push_events($self->check_checkin_copy_status());
2291 # if the circ is marked as 'claims returned', add the event to the list
2292 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2293 if ($self->circ and $self->circ->stop_fines
2294 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2296 $self->check_circ_deposit();
2298 # handle the overridable events
2299 $self->override_events unless $self->is_renewal;
2300 return if $self->bail_out;
2302 if( $self->copy and !$self->transit ) {
2304 $self->editor->search_action_transit_copy(
2305 { target_copy => $self->copy->id, dest_recv_time => undef }
2311 $self->generate_fines_finish;
2312 $self->checkin_handle_circ;
2313 return if $self->bail_out;
2314 $self->checkin_changed(1);
2316 } elsif( $self->transit ) {
2317 my $hold_transit = $self->process_received_transit;
2318 $self->checkin_changed(1);
2320 if( $self->bail_out ) {
2321 $self->checkin_flesh_events;
2325 if( my $e = $self->check_checkin_copy_status() ) {
2326 # If the original copy status is special, alert the caller
2327 my $ev = $self->events;
2328 $self->events([$e]);
2329 $self->override_events;
2330 return if $self->bail_out;
2334 if( $hold_transit or
2335 $U->copy_status($self->copy->status)->id
2336 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2339 if( $hold_transit ) {
2340 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2342 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2347 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2349 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2350 $self->reshelve_copy(1);
2351 $self->cancelled_hold_transit(1);
2352 $self->notify_hold(0); # don't notify for cancelled holds
2353 return if $self->bail_out;
2357 # hold transited to correct location
2358 $self->checkin_flesh_events;
2363 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2365 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2366 " that is in-transit, but there is no transit.. repairing");
2367 $self->reshelve_copy(1);
2368 return if $self->bail_out;
2371 if( $self->is_renewal ) {
2372 $self->finish_fines_and_voiding;
2373 return if $self->bail_out;
2374 $self->push_events(OpenILS::Event->new('SUCCESS'));
2378 # ------------------------------------------------------------------------------
2379 # Circulations and transits are now closed where necessary. Now go on to see if
2380 # this copy can fulfill a hold or needs to be routed to a different location
2381 # ------------------------------------------------------------------------------
2383 my $needed_for_something = 0; # formerly "needed_for_hold"
2385 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2387 if (!$self->remote_hold) {
2388 if ($self->use_booking) {
2389 my $potential_hold = $self->hold_capture_is_possible;
2390 my $potential_reservation = $self->reservation_capture_is_possible;
2392 if ($potential_hold and $potential_reservation) {
2393 $logger->info("circulator: item could fulfill either hold or reservation");
2394 $self->push_events(new OpenILS::Event(
2395 "HOLD_RESERVATION_CONFLICT",
2396 "hold" => $potential_hold,
2397 "reservation" => $potential_reservation
2399 return if $self->bail_out;
2400 } elsif ($potential_hold) {
2401 $needed_for_something =
2402 $self->attempt_checkin_hold_capture;
2403 } elsif ($potential_reservation) {
2404 $needed_for_something =
2405 $self->attempt_checkin_reservation_capture;
2408 $needed_for_something = $self->attempt_checkin_hold_capture;
2411 return if $self->bail_out;
2413 unless($needed_for_something) {
2414 my $circ_lib = (ref $self->copy->circ_lib) ?
2415 $self->copy->circ_lib->id : $self->copy->circ_lib;
2417 if( $self->remote_hold ) {
2418 $circ_lib = $self->remote_hold->pickup_lib;
2419 $logger->warn("circulator: Copy ".$self->copy->barcode.
2420 " is on a remote hold's shelf, sending to $circ_lib");
2423 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2425 if( $circ_lib == $self->circ_lib) {
2426 # copy is where it needs to be, either for hold or reshelving
2428 $self->checkin_handle_precat();
2429 return if $self->bail_out;
2432 # copy needs to transit "home", or stick here if it's a floating copy
2434 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2435 $self->checkin_changed(1);
2436 $self->copy->circ_lib( $self->circ_lib );
2439 my $bc = $self->copy->barcode;
2440 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2441 $self->checkin_build_copy_transit($circ_lib);
2442 return if $self->bail_out;
2443 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2447 } else { # no-op checkin
2448 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2449 $self->checkin_changed(1);
2450 $self->copy->circ_lib( $self->circ_lib );
2455 if($self->claims_never_checked_out and
2456 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2458 # the item was not supposed to be checked out to the user and should now be marked as missing
2459 $self->copy->status(OILS_COPY_STATUS_MISSING);
2463 $self->reshelve_copy unless $needed_for_something;
2466 return if $self->bail_out;
2468 unless($self->checkin_changed) {
2470 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2471 my $stat = $U->copy_status($self->copy->status)->id;
2473 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2474 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2475 $self->bail_out(1); # no need to commit anything
2479 $self->push_events(OpenILS::Event->new('SUCCESS'))
2480 unless @{$self->events};
2483 $self->finish_fines_and_voiding;
2485 OpenILS::Utils::Penalty->calculate_penalties(
2486 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2488 $self->checkin_flesh_events;
2492 sub finish_fines_and_voiding {
2494 return unless $self->circ;
2496 # gather any updates to the circ after fine generation, if there was a circ
2497 $self->generate_fines_finish;
2499 return unless $self->backdate or $self->void_overdues;
2501 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2502 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2504 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2505 $self->editor, $self->circ, $self->backdate, $note);
2507 return $self->bail_on_events($evt) if $evt;
2509 # make sure the circ isn't closed if we just voided some fines
2510 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2511 return $self->bail_on_events($evt) if $evt;
2517 # if a deposit was payed for this item, push the event
2518 sub check_circ_deposit {
2520 return unless $self->circ;
2521 my $deposit = $self->editor->search_money_billing(
2523 xact => $self->circ->id,
2525 }, {idlist => 1})->[0];
2527 $self->push_events(OpenILS::Event->new(
2528 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2533 my $force = $self->force || shift;
2534 my $copy = $self->copy;
2536 my $stat = $U->copy_status($copy->status)->id;
2539 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2540 $stat != OILS_COPY_STATUS_CATALOGING and
2541 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2542 $stat != OILS_COPY_STATUS_RESHELVING )) {
2544 $copy->status( OILS_COPY_STATUS_RESHELVING );
2546 $self->checkin_changed(1);
2551 # Returns true if the item is at the current location
2552 # because it was transited there for a hold and the
2553 # hold has not been fulfilled
2554 sub checkin_check_holds_shelf {
2556 return 0 unless $self->copy;
2559 $U->copy_status($self->copy->status)->id ==
2560 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2562 # find the hold that put us on the holds shelf
2563 my $holds = $self->editor->search_action_hold_request(
2565 current_copy => $self->copy->id,
2566 capture_time => { '!=' => undef },
2567 fulfillment_time => undef,
2568 cancel_time => undef,
2573 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2574 $self->reshelve_copy(1);
2578 my $hold = $$holds[0];
2580 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2581 $hold->id. "] for copy ".$self->copy->barcode);
2583 if( $hold->pickup_lib == $self->circ_lib ) {
2584 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2588 $logger->info("circulator: hold is not for here..");
2589 $self->remote_hold($hold);
2594 sub checkin_handle_precat {
2596 my $copy = $self->copy;
2598 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2599 $copy->status(OILS_COPY_STATUS_CATALOGING);
2600 $self->update_copy();
2601 $self->checkin_changed(1);
2602 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2607 sub checkin_build_copy_transit {
2610 my $copy = $self->copy;
2611 my $transit = Fieldmapper::action::transit_copy->new;
2613 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2614 $logger->info("circulator: transiting copy to $dest");
2616 $transit->source($self->circ_lib);
2617 $transit->dest($dest);
2618 $transit->target_copy($copy->id);
2619 $transit->source_send_time('now');
2620 $transit->copy_status( $U->copy_status($copy->status)->id );
2622 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2624 return $self->bail_on_events($self->editor->event)
2625 unless $self->editor->create_action_transit_copy($transit);
2627 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2629 $self->checkin_changed(1);
2633 sub hold_capture_is_possible {
2635 my $copy = $self->copy;
2637 # we've been explicitly told not to capture any holds
2638 return 0 if $self->capture eq 'nocapture';
2640 # See if this copy can fulfill any holds
2641 my $hold = $holdcode->find_nearest_permitted_hold(
2642 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2644 return undef if ref $hold eq "HASH" and
2645 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2649 sub reservation_capture_is_possible {
2651 my $copy = $self->copy;
2653 # we've been explicitly told not to capture any holds
2654 return 0 if $self->capture eq 'nocapture';
2656 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2657 my $resv = $booking_ses->request(
2658 "open-ils.booking.reservations.could_capture",
2659 $self->editor->authtoken, $copy->barcode
2661 $booking_ses->disconnect;
2662 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2663 $self->push_events($resv);
2669 # returns true if the item was used (or may potentially be used
2670 # in subsequent calls) to capture a hold.
2671 sub attempt_checkin_hold_capture {
2673 my $copy = $self->copy;
2675 # we've been explicitly told not to capture any holds
2676 return 0 if $self->capture eq 'nocapture';
2678 # See if this copy can fulfill any holds
2679 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2680 $self->editor, $copy, $self->editor->requestor );
2683 $logger->debug("circulator: no potential permitted".
2684 "holds found for copy ".$copy->barcode);
2688 if($self->capture ne 'capture') {
2689 # see if this item is in a hold-capture-delay location
2690 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2691 if($U->is_true($location->hold_verify)) {
2692 $self->bail_on_events(
2693 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2698 $self->retarget($retarget);
2700 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2702 $hold->current_copy($copy->id);
2703 $hold->capture_time('now');
2704 $self->put_hold_on_shelf($hold)
2705 if $hold->pickup_lib == $self->circ_lib;
2707 # prevent DB errors caused by fetching
2708 # holds from storage, and updating through cstore
2709 $hold->clear_fulfillment_time;
2710 $hold->clear_fulfillment_staff;
2711 $hold->clear_fulfillment_lib;
2712 $hold->clear_expire_time;
2713 $hold->clear_cancel_time;
2714 $hold->clear_prev_check_time unless $hold->prev_check_time;
2716 $self->bail_on_events($self->editor->event)
2717 unless $self->editor->update_action_hold_request($hold);
2719 $self->checkin_changed(1);
2721 return 0 if $self->bail_out;
2723 if( $hold->pickup_lib == $self->circ_lib ) {
2725 # This hold was captured in the correct location
2726 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2727 $self->push_events(OpenILS::Event->new('SUCCESS'));
2729 #$self->do_hold_notify($hold->id);
2730 $self->notify_hold($hold->id);
2734 # Hold needs to be picked up elsewhere. Build a hold
2735 # transit and route the item.
2736 $self->checkin_build_hold_transit();
2737 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2738 return 0 if $self->bail_out;
2739 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2742 # make sure we save the copy status
2747 sub attempt_checkin_reservation_capture {
2749 my $copy = $self->copy;
2751 # we've been explicitly told not to capture any holds
2752 return 0 if $self->capture eq 'nocapture';
2754 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2755 my $evt = $booking_ses->request(
2756 "open-ils.booking.resources.capture_for_reservation",
2757 $self->editor->authtoken,
2759 1 # don't update copy - we probably have it locked
2761 $booking_ses->disconnect;
2763 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2765 "open-ils.booking.resources.capture_for_reservation " .
2766 "didn't return an event!"
2770 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2771 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2773 # not-transferable is an error event we'll pass on the user
2774 $logger->warn("reservation capture attempted against non-transferable item");
2775 $self->push_events($evt);
2777 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2778 # Re-retrieve copy as reservation capture may have changed
2779 # its status and whatnot.
2781 "circulator: booking capture win on copy " . $self->copy->id
2783 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2785 "circulator: changing copy " . $self->copy->id .
2786 "'s status from " . $self->copy->status . " to " .
2789 $self->copy->status($new_copy_status);
2792 $self->reservation($evt->{"payload"}->{"reservation"});
2794 if (exists $evt->{"payload"}->{"transit"}) {
2798 "org" => $evt->{"payload"}->{"transit"}->dest
2802 $self->checkin_changed(1);
2806 # other results are treated as "nothing to capture"
2810 sub do_hold_notify {
2811 my( $self, $holdid ) = @_;
2813 my $e = new_editor(xact => 1);
2814 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2816 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2817 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2819 $logger->info("circulator: running delayed hold notify process");
2821 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2822 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2824 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2825 hold_id => $holdid, requestor => $self->editor->requestor);
2827 $logger->debug("circulator: built hold notifier");
2829 if(!$notifier->event) {
2831 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2833 my $stat = $notifier->send_email_notify;
2834 if( $stat == '1' ) {
2835 $logger->info("circulator: hold notify succeeded for hold $holdid");
2839 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2842 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2846 sub retarget_holds {
2848 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2849 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2850 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2851 # no reason to wait for the return value
2855 sub checkin_build_hold_transit {
2858 my $copy = $self->copy;
2859 my $hold = $self->hold;
2860 my $trans = Fieldmapper::action::hold_transit_copy->new;
2862 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2864 $trans->hold($hold->id);
2865 $trans->source($self->circ_lib);
2866 $trans->dest($hold->pickup_lib);
2867 $trans->source_send_time("now");
2868 $trans->target_copy($copy->id);
2870 # when the copy gets to its destination, it will recover
2871 # this status - put it onto the holds shelf
2872 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2874 return $self->bail_on_events($self->editor->event)
2875 unless $self->editor->create_action_hold_transit_copy($trans);
2880 sub process_received_transit {
2882 my $copy = $self->copy;
2883 my $copyid = $self->copy->id;
2885 my $status_name = $U->copy_status($copy->status)->name;
2886 $logger->debug("circulator: attempting transit receive on ".
2887 "copy $copyid. Copy status is $status_name");
2889 my $transit = $self->transit;
2891 if( $transit->dest != $self->circ_lib ) {
2892 # - this item is in-transit to a different location
2894 my $tid = $transit->id;
2895 my $loc = $self->circ_lib;
2896 my $dest = $transit->dest;
2898 $logger->info("circulator: Fowarding transit on copy which is destined ".
2899 "for a different location. transit=$tid, copy=$copyid, current ".
2900 "location=$loc, destination location=$dest");
2902 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2904 # grab the associated hold object if available
2905 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2906 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2908 return $self->bail_on_events($evt);
2911 # The transit is received, set the receive time
2912 $transit->dest_recv_time('now');
2913 $self->bail_on_events($self->editor->event)
2914 unless $self->editor->update_action_transit_copy($transit);
2916 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2918 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2919 $copy->status( $transit->copy_status );
2920 $self->update_copy();
2921 return if $self->bail_out;
2925 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2927 # hold has arrived at destination, set shelf time
2928 $self->put_hold_on_shelf($hold);
2929 $self->bail_on_events($self->editor->event)
2930 unless $self->editor->update_action_hold_request($hold);
2931 return if $self->bail_out;
2933 $self->notify_hold($hold_transit->hold);
2938 OpenILS::Event->new(
2941 payload => { transit => $transit, holdtransit => $hold_transit } ));
2943 return $hold_transit;
2947 # ------------------------------------------------------------------
2948 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2949 # ------------------------------------------------------------------
2950 sub put_hold_on_shelf {
2951 my($self, $hold) = @_;
2953 $hold->shelf_time('now');
2955 my $shelf_expire = $U->ou_ancestor_setting_value(
2956 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2958 return undef unless $shelf_expire;
2960 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2961 my $expire_time = DateTime->now->add(seconds => $seconds);
2963 # if the shelf expire time overlaps with a pickup lib's
2964 # closed date, push it out to the first open date
2965 my $dateinfo = $U->storagereq(
2966 'open-ils.storage.actor.org_unit.closed_date.overlap',
2967 $hold->pickup_lib, $expire_time);
2970 my $dt_parser = DateTime::Format::ISO8601->new;
2971 $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
2973 # TODO: enable/disable time bump via setting?
2974 $expire_time->set(hour => '23', minute => '59', second => '59');
2976 $logger->info("circulator: shelf_expire_time overlaps".
2977 " with closed date, pushing expire time to $expire_time");
2980 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2986 sub generate_fines {
2988 my $reservation = shift;
2990 $self->generate_fines_start($reservation);
2991 $self->generate_fines_finish($reservation);
2996 sub generate_fines_start {
2998 my $reservation = shift;
2999 my $dt_parser = DateTime::Format::ISO8601->new;
3001 my $obj = $reservation ? $self->reservation : $self->circ;
3003 # If we have a grace period
3004 if($obj->can('grace_period')) {
3005 # Parse out the due date
3006 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3007 # Add the grace period to the due date
3008 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3009 # Don't generate fines on circs still in grace period
3010 return undef if ($due_date > DateTime->now);
3013 if (!exists($self->{_gen_fines_req})) {
3014 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3016 'open-ils.storage.action.circulation.overdue.generate_fines',
3024 sub generate_fines_finish {
3026 my $reservation = shift;
3028 return undef unless $self->{_gen_fines_req};
3030 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3032 $self->{_gen_fines_req}->wait_complete;
3033 delete($self->{_gen_fines_req});
3035 # refresh the circ in case the fine generator set the stop_fines field
3036 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3037 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3042 sub checkin_handle_circ {
3044 my $circ = $self->circ;
3045 my $copy = $self->copy;
3049 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3051 # backdate the circ if necessary
3052 if($self->backdate) {
3053 my $evt = $self->checkin_handle_backdate;
3054 return $self->bail_on_events($evt) if $evt;
3057 if(!$circ->stop_fines) {
3058 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3059 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3060 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3061 $circ->stop_fines_time('now');
3062 $circ->stop_fines_time($self->backdate) if $self->backdate;
3065 # Set the checkin vars since we have the item
3066 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3068 # capture the true scan time for back-dated checkins
3069 $circ->checkin_scan_time('now');
3071 $circ->checkin_staff($self->editor->requestor->id);
3072 $circ->checkin_lib($self->circ_lib);
3073 $circ->checkin_workstation($self->editor->requestor->wsid);
3075 my $circ_lib = (ref $self->copy->circ_lib) ?
3076 $self->copy->circ_lib->id : $self->copy->circ_lib;
3077 my $stat = $U->copy_status($self->copy->status)->id;
3079 # immediately available keeps items lost or missing items from going home before being handled
3080 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3081 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3084 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3086 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3087 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3089 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3093 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3095 $self->checkin_handle_lost($circ_lib);
3099 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3104 # see if there are any fines owed on this circ. if not, close it
3105 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3106 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3108 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3110 return $self->bail_on_events($self->editor->event)
3111 unless $self->editor->update_action_circulation($circ);
3117 # ------------------------------------------------------------------
3118 # See if we need to void billings for lost checkin
3119 # ------------------------------------------------------------------
3120 sub checkin_handle_lost {
3122 my $circ_lib = shift;
3123 my $circ = $self->circ;
3125 my $max_return = $U->ou_ancestor_setting_value(
3126 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3131 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3132 $tm[5] -= 1 if $tm[5] > 0;
3133 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3135 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3136 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3138 $max_return = 0 if $today < $last_chance;
3141 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3143 my $void_lost = $U->ou_ancestor_setting_value(
3144 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3145 my $void_lost_fee = $U->ou_ancestor_setting_value(
3146 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3147 my $restore_od = $U->ou_ancestor_setting_value(
3148 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3149 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3150 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3152 $self->checkin_handle_lost_now_found(3) if $void_lost;
3153 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3154 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3157 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3162 sub checkin_handle_backdate {
3165 # ------------------------------------------------------------------
3166 # clean up the backdate for date comparison
3167 # XXX We are currently taking the due-time from the original due-date,
3168 # not the input. Do we need to do this? This certainly interferes with
3169 # backdating of hourly checkouts, but that is likely a very rare case.
3170 # ------------------------------------------------------------------
3171 my $bd = cleanse_ISO8601($self->backdate);
3172 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3173 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3174 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3176 $self->backdate($bd);
3181 sub check_checkin_copy_status {
3183 my $copy = $self->copy;
3185 my $status = $U->copy_status($copy->status)->id;
3188 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3189 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3190 $status == OILS_COPY_STATUS_IN_PROCESS ||
3191 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3192 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3193 $status == OILS_COPY_STATUS_CATALOGING ||
3194 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3195 $status == OILS_COPY_STATUS_RESHELVING );
3197 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3198 if( $status == OILS_COPY_STATUS_LOST );
3200 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3201 if( $status == OILS_COPY_STATUS_MISSING );
3203 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3208 # --------------------------------------------------------------------------
3209 # On checkin, we need to return as many relevant objects as we can
3210 # --------------------------------------------------------------------------
3211 sub checkin_flesh_events {
3214 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3215 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3216 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3219 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3222 if($self->hold and !$self->hold->cancel_time) {
3223 $hold = $self->hold;
3224 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3228 # if we checked in a circulation, flesh the billing summary data
3229 $self->circ->billable_transaction(
3230 $self->editor->retrieve_money_billable_transaction([
3232 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3238 # flesh some patron fields before returning
3240 $self->editor->retrieve_actor_user([
3245 au => ['card', 'billing_address', 'mailing_address']
3252 for my $evt (@{$self->events}) {
3255 $payload->{copy} = $U->unflesh_copy($self->copy);
3256 $payload->{volume} = $self->volume;
3257 $payload->{record} = $record,
3258 $payload->{circ} = $self->circ;
3259 $payload->{transit} = $self->transit;
3260 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3261 $payload->{hold} = $hold;
3262 $payload->{patron} = $self->patron;
3263 $payload->{reservation} = $self->reservation
3264 unless (not $self->reservation or $self->reservation->cancel_time);
3266 $evt->{payload} = $payload;
3271 my( $self, $msg ) = @_;
3272 my $bc = ($self->copy) ? $self->copy->barcode :
3275 my $usr = ($self->patron) ? $self->patron->id : "";
3276 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3277 ", recipient=$usr, copy=$bc");
3283 $self->log_me("do_renew()");
3285 # Make sure there is an open circ to renew that is not
3286 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3287 my $usrid = $self->patron->id if $self->patron;
3288 my $circ = $self->editor->search_action_circulation({
3289 target_copy => $self->copy->id,
3290 xact_finish => undef,
3291 checkin_time => undef,
3292 ($usrid ? (usr => $usrid) : ()),
3294 {stop_fines => undef},
3295 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3299 return $self->bail_on_events($self->editor->event) unless $circ;
3301 # A user is not allowed to renew another user's items without permission
3302 unless( $circ->usr eq $self->editor->requestor->id ) {
3303 return $self->bail_on_events($self->editor->events)
3304 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3307 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3308 if $circ->renewal_remaining < 1;
3310 # -----------------------------------------------------------------
3312 $self->parent_circ($circ->id);
3313 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3316 # Run the fine generator against the old circ
3317 $self->generate_fines_start;
3319 $self->run_renew_permit;
3322 $self->do_checkin();
3323 return if $self->bail_out;
3325 unless( $self->permit_override ) {
3327 return if $self->bail_out;
3328 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3329 $self->remove_event('ITEM_NOT_CATALOGED');
3332 $self->override_events;
3333 return if $self->bail_out;
3336 $self->do_checkout();
3341 my( $self, $evt ) = @_;
3342 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3343 $logger->debug("circulator: removing event from list: $evt");
3344 my @events = @{$self->events};
3345 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3350 my( $self, $evt ) = @_;
3351 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3352 return grep { $_->{textcode} eq $evt } @{$self->events};
3357 sub run_renew_permit {
3360 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3361 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3362 $self->editor, $self->copy, $self->editor->requestor, 1
3364 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3367 if(!$self->legacy_script_support) {
3368 my $results = $self->run_indb_circ_test;
3369 $self->push_events($self->matrix_test_result_events)
3370 unless $self->circ_test_success;
3373 my $runner = $self->script_runner;
3375 $runner->load($self->circ_permit_renew);
3376 my $result = $runner->run or
3377 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3378 if ($result->{"events"}) {
3380 map { new OpenILS::Event($_) } @{$result->{"events"}}
3383 "circulator: circ_permit_renew for user " .
3384 $self->patron->id . " returned " .
3385 scalar(@{$result->{"events"}}) . " event(s)"
3389 $self->mk_script_runner;
3392 $logger->debug("circulator: re-creating script runner to be safe");
3396 # XXX: The primary mechanism for storing circ history is now handled
3397 # by tracking real circulation objects instead of bibs in a bucket.
3398 # However, this code is disabled by default and could be useful
3399 # some day, so may as well leave it for now.
3400 sub append_reading_list {
3404 $self->is_checkout and
3410 # verify history is globally enabled and uses the bucket mechanism
3411 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3412 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3414 return undef unless $htype and $htype eq 'bucket';
3416 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3418 # verify the patron wants to retain the hisory
3419 my $setting = $e->search_actor_user_setting(
3420 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3422 unless($setting and $setting->value) {
3427 my $bkt = $e->search_container_copy_bucket(
3428 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3433 # find the next item position
3434 my $last_item = $e->search_container_copy_bucket_item(
3435 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3436 $pos = $last_item->pos + 1 if $last_item;
3439 # create the history bucket if necessary
3440 $bkt = Fieldmapper::container::copy_bucket->new;
3441 $bkt->owner($self->patron->id);
3443 $bkt->btype('circ_history');
3445 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3448 my $item = Fieldmapper::container::copy_bucket_item->new;
3450 $item->bucket($bkt->id);
3451 $item->target_copy($self->copy->id);
3454 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3461 sub make_trigger_events {
3463 return unless $self->circ;
3464 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3465 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3466 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3471 sub checkin_handle_lost_now_found {
3472 my ($self, $bill_type) = @_;
3474 # ------------------------------------------------------------------
3475 # remove charge from patron's account if lost item is returned
3476 # ------------------------------------------------------------------
3478 my $bills = $self->editor->search_money_billing(
3480 xact => $self->circ->id,
3485 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3486 for my $bill (@$bills) {
3487 if( !$U->is_true($bill->voided) ) {
3488 $logger->info("lost item returned - voiding bill ".$bill->id);
3490 $bill->void_time('now');
3491 $bill->voider($self->editor->requestor->id);
3492 my $note = ($bill->note) ? $bill->note . "\n" : '';
3493 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3495 $self->bail_on_events($self->editor->event)
3496 unless $self->editor->update_money_billing($bill);
3501 sub checkin_handle_lost_now_found_restore_od {
3503 my $circ_lib = shift;
3505 # ------------------------------------------------------------------
3506 # restore those overdue charges voided when item was set to lost
3507 # ------------------------------------------------------------------
3509 my $ods = $self->editor->search_money_billing(
3511 xact => $self->circ->id,
3516 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3517 for my $bill (@$ods) {
3518 if( $U->is_true($bill->voided) ) {
3519 $logger->info("lost item returned - restoring overdue ".$bill->id);
3521 $bill->clear_void_time;
3522 $bill->voider($self->editor->requestor->id);
3523 my $note = ($bill->note) ? $bill->note . "\n" : '';
3524 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3526 $self->bail_on_events($self->editor->event)
3527 unless $self->editor->update_money_billing($bill);
3532 # ------------------------------------------------------------------
3533 # Lost-then-found item checked in. This sub generates new overdue
3534 # fines, beyond the point of any existing and possibly voided
3535 # overdue fines, up to the point of final checkin time (or max fine
3537 # ------------------------------------------------------------------
3538 sub generate_lost_overdue_fines {
3540 my $circ = $self->circ;
3541 my $e = $self->editor;
3543 # Re-open the transaction so the fine generator can see it
3544 if($circ->xact_finish or $circ->stop_fines) {
3546 $circ->clear_xact_finish;
3547 $circ->clear_stop_fines;
3548 $circ->clear_stop_fines_time;
3549 $e->update_action_circulation($circ) or return $e->die_event;
3553 $e->xact_begin; # generate_fines expects an in-xact editor
3554 $self->generate_fines;
3555 $circ = $self->circ; # generate fines re-fetches the circ
3559 # Re-close the transaction if no money is owed
3560 my ($obt) = $U->fetch_mbts($circ->id, $e);
3561 if ($obt and $obt->balance_owed == 0) {
3562 $circ->xact_finish('now');
3566 # Set stop fines if the fine generator didn't have to
3567 unless($circ->stop_fines) {
3568 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3569 $circ->stop_fines_time('now');
3573 # update the event data sent to the caller within the transaction
3574 $self->checkin_flesh_events;
3577 $e->update_action_circulation($circ) or return $e->die_event;