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->retrieve_actor_user($card->usr)
768 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
770 # Use the card we looked up, not the patron's primary, for card active checks
771 $patron->card($card);
774 if( my $copy = $self->copy ) {
777 $flesh->{flesh_fields}->{circ} = ['usr'];
779 my $circ = $e->search_action_circulation([
780 {target_copy => $copy->id, checkin_time => undef}, $flesh
784 $patron = $circ->usr;
785 $circ->usr($patron->id); # de-flesh for consistency
791 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
792 unless $self->patron($patron) or $self->is_checkin;
794 unless($self->is_checkin) {
796 # Check for inactivity and patron reg. expiration
798 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
799 unless $U->is_true($patron->active);
801 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
802 unless $U->is_true($patron->card->active);
804 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
805 cleanse_ISO8601($patron->expire_date));
807 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
808 if( CORE::time > $expire->epoch ) ;
812 # --------------------------------------------------------------------------
813 # This builds the script runner environment and fetches most of the
815 # --------------------------------------------------------------------------
816 sub mk_script_runner {
822 qw/copy copy_barcode copy_id patron
823 patron_id patron_barcode volume title editor/;
825 # Translate our objects into the ScriptBuilder args hash
826 $$args{$_} = $self->$_() for @fields;
828 $args->{ignore_user_status} = 1 if $self->is_checkin;
829 $$args{fetch_patron_by_circ_copy} = 1;
830 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
832 if( my $pco = $self->pending_checkouts ) {
833 $logger->info("circulator: we were given a pending checkouts number of $pco");
834 $$args{patronItemsOut} = $pco;
837 # This fetches most of the objects we need
838 $self->script_runner(
839 OpenILS::Application::Circ::ScriptBuilder->build($args));
841 # Now we translate the ScriptBuilder objects back into self
842 $self->$_($$args{$_}) for @fields;
844 my @evts = @{$args->{_events}} if $args->{_events};
846 $logger->debug("circulator: script builder returned events: @evts") if @evts;
850 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
851 if(!$self->is_noncat and
853 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
857 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
858 return $self->bail_on_events(@e);
863 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
864 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
865 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
866 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
870 # We can't renew if there is no copy
871 return $self->bail_on_events(@evts) if
872 $self->is_renewal and !$self->copy;
874 # Set some circ-specific flags in the script environment
875 my $evt = "environment";
876 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
878 if( $self->is_noncat ) {
879 $self->script_runner->insert("$evt.isNonCat", 1);
880 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
883 if( $self->is_precat ) {
884 $self->script_runner->insert("environment.isPrecat", 1, 1);
887 $self->script_runner->add_path( $_ ) for @$script_libs;
892 # --------------------------------------------------------------------------
893 # Does the circ permit work
894 # --------------------------------------------------------------------------
898 $self->log_me("do_permit()");
900 unless( $self->editor->requestor->id == $self->patron->id ) {
901 return $self->bail_on_events($self->editor->event)
902 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
905 $self->check_captured_holds();
906 $self->do_copy_checks();
907 return if $self->bail_out;
908 $self->run_patron_permit_scripts();
909 $self->run_copy_permit_scripts()
910 unless $self->is_precat or $self->is_noncat;
911 $self->check_item_deposit_events();
912 $self->override_events();
913 return if $self->bail_out;
915 if($self->is_precat and not $self->request_precat) {
918 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
919 return $self->bail_out(1) unless $self->is_renewal;
923 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
926 sub check_item_deposit_events {
928 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
929 if $self->is_deposit and not $self->is_deposit_exempt;
930 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
931 if $self->is_rental and not $self->is_rental_exempt;
934 # returns true if the user is not required to pay deposits
935 sub is_deposit_exempt {
937 my $pid = (ref $self->patron->profile) ?
938 $self->patron->profile->id : $self->patron->profile;
939 my $groups = $U->ou_ancestor_setting_value(
940 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
941 for my $grp (@$groups) {
942 return 1 if $self->is_group_descendant($grp, $pid);
947 # returns true if the user is not required to pay rental fees
948 sub is_rental_exempt {
950 my $pid = (ref $self->patron->profile) ?
951 $self->patron->profile->id : $self->patron->profile;
952 my $groups = $U->ou_ancestor_setting_value(
953 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
954 for my $grp (@$groups) {
955 return 1 if $self->is_group_descendant($grp, $pid);
960 sub is_group_descendant {
961 my($self, $p_id, $c_id) = @_;
962 return 0 unless defined $p_id and defined $c_id;
963 return 1 if $c_id == $p_id;
964 while(my $grp = $user_groups{$c_id}) {
965 $c_id = $grp->parent;
966 return 0 unless defined $c_id;
967 return 1 if $c_id == $p_id;
972 sub check_captured_holds {
974 my $copy = $self->copy;
975 my $patron = $self->patron;
977 return undef unless $copy;
979 my $s = $U->copy_status($copy->status)->id;
980 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
981 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
983 # Item is on the holds shelf, make sure it's going to the right person
984 my $holds = $self->editor->search_action_hold_request(
987 current_copy => $copy->id ,
988 capture_time => { '!=' => undef },
989 cancel_time => undef,
990 fulfillment_time => undef
996 if( $holds and $$holds[0] ) {
997 return undef if $$holds[0]->usr == $patron->id;
1000 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1002 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1006 sub do_copy_checks {
1008 my $copy = $self->copy;
1009 return unless $copy;
1011 my $stat = $U->copy_status($copy->status)->id;
1013 # We cannot check out a copy if it is in-transit
1014 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1015 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1018 $self->handle_claims_returned();
1019 return if $self->bail_out;
1021 # no claims returned circ was found, check if there is any open circ
1022 unless( $self->is_renewal ) {
1024 my $circs = $self->editor->search_action_circulation(
1025 { target_copy => $copy->id, checkin_time => undef }
1028 if(my $old_circ = $circs->[0]) { # an open circ was found
1030 my $payload = {copy => $copy};
1032 if($old_circ->usr == $self->patron->id) {
1034 $payload->{old_circ} = $old_circ;
1036 # If there is an open circulation on the checkout item and an auto-renew
1037 # interval is defined, inform the caller that they should go
1038 # ahead and renew the item instead of warning about open circulations.
1040 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1042 'circ.checkout_auto_renew_age',
1046 if($auto_renew_intvl) {
1047 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1048 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1050 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1051 $payload->{auto_renew} = 1;
1056 return $self->bail_on_events(
1057 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1063 my $LEGACY_CIRC_EVENT_MAP = {
1064 'no_item' => 'ITEM_NOT_CATALOGED',
1065 'actor.usr.barred' => 'PATRON_BARRED',
1066 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1067 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1068 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1069 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1070 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1071 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1072 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1073 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1077 # ---------------------------------------------------------------------
1078 # This pushes any patron-related events into the list but does not
1079 # set bail_out for any events
1080 # ---------------------------------------------------------------------
1081 sub run_patron_permit_scripts {
1083 my $runner = $self->script_runner;
1084 my $patronid = $self->patron->id;
1088 if(!$self->legacy_script_support) {
1090 my $results = $self->run_indb_circ_test;
1091 unless($self->circ_test_success) {
1092 # no_item result is OK during noncat checkout
1093 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1094 push @allevents, $self->matrix_test_result_events;
1100 # ---------------------------------------------------------------------
1101 # # Now run the patron permit script
1102 # ---------------------------------------------------------------------
1103 $runner->load($self->circ_permit_patron);
1104 my $result = $runner->run or
1105 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1107 my $patron_events = $result->{events};
1109 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1110 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1111 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1112 $penalties = $penalties->{fatal_penalties};
1114 for my $pen (@$penalties) {
1115 my $event = OpenILS::Event->new($pen->name);
1116 $event->{desc} = $pen->label;
1117 push(@allevents, $event);
1120 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1124 $_->{payload} = $self->copy if
1125 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1128 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1130 $self->push_events(@allevents);
1133 sub matrix_test_result_codes {
1135 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1138 sub matrix_test_result_events {
1141 my $event = new OpenILS::Event(
1142 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1144 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1146 } (@{$self->matrix_test_result});
1149 sub run_indb_circ_test {
1151 return $self->matrix_test_result if $self->matrix_test_result;
1153 my $dbfunc = ($self->is_renewal) ?
1154 'action.item_user_renew_test' : 'action.item_user_circ_test';
1156 if( $self->is_precat && $self->request_precat) {
1157 $self->make_precat_copy;
1158 return if $self->bail_out;
1161 my $results = $self->editor->json_query(
1165 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1171 $self->circ_test_success($U->is_true($results->[0]->{success}));
1173 if(my $mp = $results->[0]->{matchpoint}) {
1174 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1175 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1176 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1177 if(defined($results->[0]->{renewals})) {
1178 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1180 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1181 if(defined($results->[0]->{grace_period})) {
1182 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1184 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1185 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1188 return $self->matrix_test_result($results);
1191 # ---------------------------------------------------------------------
1192 # given a use and copy, this will calculate the circulation policy
1193 # parameters. Only works with in-db circ.
1194 # ---------------------------------------------------------------------
1198 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1200 $self->run_indb_circ_test;
1203 circ_test_success => $self->circ_test_success,
1204 failure_events => [],
1205 failure_codes => [],
1206 matchpoint => $self->circ_matrix_matchpoint
1209 unless($self->circ_test_success) {
1210 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1211 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1214 if($self->circ_matrix_matchpoint) {
1215 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1216 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1217 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1218 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1220 my $policy = $self->get_circ_policy(
1221 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1223 $$results{$_} = $$policy{$_} for keys %$policy;
1229 # ---------------------------------------------------------------------
1230 # Loads the circ policy info for duration, recurring fine, and max
1231 # fine based on the current copy
1232 # ---------------------------------------------------------------------
1233 sub get_circ_policy {
1234 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1237 duration_rule => $duration_rule->name,
1238 recurring_fine_rule => $recurring_fine_rule->name,
1239 max_fine_rule => $max_fine_rule->name,
1240 max_fine => $self->get_max_fine_amount($max_fine_rule),
1241 fine_interval => $recurring_fine_rule->recurrence_interval,
1242 renewal_remaining => $duration_rule->max_renewals,
1243 grace_period => $recurring_fine_rule->grace_period
1246 if($hard_due_date) {
1247 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1248 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1251 $policy->{duration_date_ceiling} = undef;
1252 $policy->{duration_date_ceiling_force} = undef;
1255 $policy->{duration} = $duration_rule->shrt
1256 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1257 $policy->{duration} = $duration_rule->normal
1258 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1259 $policy->{duration} = $duration_rule->extended
1260 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1262 $policy->{recurring_fine} = $recurring_fine_rule->low
1263 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1264 $policy->{recurring_fine} = $recurring_fine_rule->normal
1265 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1266 $policy->{recurring_fine} = $recurring_fine_rule->high
1267 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1272 sub get_max_fine_amount {
1274 my $max_fine_rule = shift;
1275 my $max_amount = $max_fine_rule->amount;
1277 # if is_percent is true then the max->amount is
1278 # use as a percentage of the copy price
1279 if ($U->is_true($max_fine_rule->is_percent)) {
1280 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1281 $max_amount = $price * $max_fine_rule->amount / 100;
1283 $U->ou_ancestor_setting_value(
1285 'circ.max_fine.cap_at_price',
1289 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1290 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1298 sub run_copy_permit_scripts {
1300 my $copy = $self->copy || return;
1301 my $runner = $self->script_runner;
1305 if(!$self->legacy_script_support) {
1306 my $results = $self->run_indb_circ_test;
1307 push @allevents, $self->matrix_test_result_events
1308 unless $self->circ_test_success;
1311 # ---------------------------------------------------------------------
1312 # Capture all of the copy permit events
1313 # ---------------------------------------------------------------------
1314 $runner->load($self->circ_permit_copy);
1315 my $result = $runner->run or
1316 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1317 my $copy_events = $result->{events};
1319 # ---------------------------------------------------------------------
1320 # Now collect all of the events together
1321 # ---------------------------------------------------------------------
1322 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1325 # See if this copy has an alert message
1326 my $ae = $self->check_copy_alert();
1327 push( @allevents, $ae ) if $ae;
1329 # uniquify the events
1330 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1331 @allevents = values %hash;
1333 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1335 $self->push_events(@allevents);
1339 sub check_copy_alert {
1341 return undef if $self->is_renewal;
1342 return OpenILS::Event->new(
1343 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1344 if $self->copy and $self->copy->alert_message;
1350 # --------------------------------------------------------------------------
1351 # If the call is overriding and has permissions to override every collected
1352 # event, the are cleared. Any event that the caller does not have
1353 # permission to override, will be left in the event list and bail_out will
1355 # XXX We need code in here to cancel any holds/transits on copies
1356 # that are being force-checked out
1357 # --------------------------------------------------------------------------
1358 sub override_events {
1360 my @events = @{$self->events};
1361 return unless @events;
1363 if(!$self->override) {
1364 return $self->bail_out(1)
1365 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1370 for my $e (@events) {
1371 my $tc = $e->{textcode};
1372 next if $tc eq 'SUCCESS';
1373 my $ov = "$tc.override";
1374 $logger->info("circulator: attempting to override event: $ov");
1376 return $self->bail_on_events($self->editor->event)
1377 unless( $self->editor->allowed($ov) );
1382 # --------------------------------------------------------------------------
1383 # If there is an open claimsreturn circ on the requested copy, close the
1384 # circ if overriding, otherwise bail out
1385 # --------------------------------------------------------------------------
1386 sub handle_claims_returned {
1388 my $copy = $self->copy;
1390 my $CR = $self->editor->search_action_circulation(
1392 target_copy => $copy->id,
1393 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1394 checkin_time => undef,
1398 return unless ($CR = $CR->[0]);
1402 # - If the caller has set the override flag, we will check the item in
1403 if($self->override) {
1405 $CR->checkin_time('now');
1406 $CR->checkin_scan_time('now');
1407 $CR->checkin_lib($self->circ_lib);
1408 $CR->checkin_workstation($self->editor->requestor->wsid);
1409 $CR->checkin_staff($self->editor->requestor->id);
1411 $evt = $self->editor->event
1412 unless $self->editor->update_action_circulation($CR);
1415 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1418 $self->bail_on_events($evt) if $evt;
1423 # --------------------------------------------------------------------------
1424 # This performs the checkout
1425 # --------------------------------------------------------------------------
1429 $self->log_me("do_checkout()");
1431 # make sure perms are good if this isn't a renewal
1432 unless( $self->is_renewal ) {
1433 return $self->bail_on_events($self->editor->event)
1434 unless( $self->editor->allowed('COPY_CHECKOUT') );
1437 # verify the permit key
1438 unless( $self->check_permit_key ) {
1439 if( $self->permit_override ) {
1440 return $self->bail_on_events($self->editor->event)
1441 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1443 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1447 # if this is a non-cataloged circ, build the circ and finish
1448 if( $self->is_noncat ) {
1449 $self->checkout_noncat;
1451 OpenILS::Event->new('SUCCESS',
1452 payload => { noncat_circ => $self->circ }));
1456 if( $self->is_precat ) {
1457 $self->make_precat_copy;
1458 return if $self->bail_out;
1460 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1461 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1464 $self->do_copy_checks;
1465 return if $self->bail_out;
1467 $self->run_checkout_scripts();
1468 return if $self->bail_out;
1470 $self->build_checkout_circ_object();
1471 return if $self->bail_out;
1473 my $modify_to_start = $self->booking_adjusted_due_date();
1474 return if $self->bail_out;
1476 $self->apply_modified_due_date($modify_to_start);
1477 return if $self->bail_out;
1479 return $self->bail_on_events($self->editor->event)
1480 unless $self->editor->create_action_circulation($self->circ);
1482 # refresh the circ to force local time zone for now
1483 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1485 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1487 return if $self->bail_out;
1489 $self->apply_deposit_fee();
1490 return if $self->bail_out;
1492 $self->handle_checkout_holds();
1493 return if $self->bail_out;
1495 # ------------------------------------------------------------------------------
1496 # Update the patron penalty info in the DB. Run it for permit-overrides
1497 # since the penalties are not updated during the permit phase
1498 # ------------------------------------------------------------------------------
1499 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1501 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1504 if($self->is_renewal) {
1505 # flesh the billing summary for the checked-in circ
1506 $pcirc = $self->editor->retrieve_action_circulation([
1508 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1513 OpenILS::Event->new('SUCCESS',
1515 copy => $U->unflesh_copy($self->copy),
1516 volume => $self->volume,
1517 circ => $self->circ,
1519 holds_fulfilled => $self->fulfilled_holds,
1520 deposit_billing => $self->deposit_billing,
1521 rental_billing => $self->rental_billing,
1522 parent_circ => $pcirc,
1523 patron => ($self->return_patron) ? $self->patron : undef,
1524 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1530 sub apply_deposit_fee {
1532 my $copy = $self->copy;
1534 ($self->is_deposit and not $self->is_deposit_exempt) or
1535 ($self->is_rental and not $self->is_rental_exempt);
1537 return if $self->is_deposit and $self->skip_deposit_fee;
1538 return if $self->is_rental and $self->skip_rental_fee;
1540 my $bill = Fieldmapper::money::billing->new;
1541 my $amount = $copy->deposit_amount;
1545 if($self->is_deposit) {
1546 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1548 $self->deposit_billing($bill);
1550 $billing_type = OILS_BILLING_TYPE_RENTAL;
1552 $self->rental_billing($bill);
1555 $bill->xact($self->circ->id);
1556 $bill->amount($amount);
1557 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1558 $bill->billing_type($billing_type);
1559 $bill->btype($btype);
1560 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1562 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1567 my $copy = $self->copy;
1569 my $stat = $copy->status if ref $copy->status;
1570 my $loc = $copy->location if ref $copy->location;
1571 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1573 $copy->status($stat->id) if $stat;
1574 $copy->location($loc->id) if $loc;
1575 $copy->circ_lib($circ_lib->id) if $circ_lib;
1576 $copy->editor($self->editor->requestor->id);
1577 $copy->edit_date('now');
1578 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1580 return $self->bail_on_events($self->editor->event)
1581 unless $self->editor->update_asset_copy($self->copy);
1583 $copy->status($U->copy_status($copy->status));
1584 $copy->location($loc) if $loc;
1585 $copy->circ_lib($circ_lib) if $circ_lib;
1588 sub update_reservation {
1590 my $reservation = $self->reservation;
1592 my $usr = $reservation->usr;
1593 my $target_rt = $reservation->target_resource_type;
1594 my $target_r = $reservation->target_resource;
1595 my $current_r = $reservation->current_resource;
1597 $reservation->usr($usr->id) if ref $usr;
1598 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1599 $reservation->target_resource($target_r->id) if ref $target_r;
1600 $reservation->current_resource($current_r->id) if ref $current_r;
1602 return $self->bail_on_events($self->editor->event)
1603 unless $self->editor->update_booking_reservation($self->reservation);
1606 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1607 $self->reservation($reservation);
1611 sub bail_on_events {
1612 my( $self, @evts ) = @_;
1613 $self->push_events(@evts);
1618 # ------------------------------------------------------------------------------
1619 # When an item is checked out, see if we can fulfill a hold for this patron
1620 # ------------------------------------------------------------------------------
1621 sub handle_checkout_holds {
1623 my $copy = $self->copy;
1624 my $patron = $self->patron;
1626 my $e = $self->editor;
1627 $self->fulfilled_holds([]);
1629 # pre/non-cats can't fulfill a hold
1630 return if $self->is_precat or $self->is_noncat;
1632 my $hold = $e->search_action_hold_request({
1633 current_copy => $copy->id ,
1634 cancel_time => undef,
1635 fulfillment_time => undef,
1637 {expire_time => undef},
1638 {expire_time => {'>' => 'now'}}
1642 if($hold and $hold->usr != $patron->id) {
1643 # reset the hold since the copy is now checked out
1645 $logger->info("circulator: un-targeting hold ".$hold->id.
1646 " because copy ".$copy->id." is getting checked out");
1648 $hold->clear_prev_check_time;
1649 $hold->clear_current_copy;
1650 $hold->clear_capture_time;
1652 return $self->bail_on_event($e->event)
1653 unless $e->update_action_hold_request($hold);
1659 $hold = $self->find_related_user_hold($copy, $patron) or return;
1660 $logger->info("circulator: found related hold to fulfill in checkout");
1663 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1665 # if the hold was never officially captured, capture it.
1666 $hold->current_copy($copy->id);
1667 $hold->capture_time('now') unless $hold->capture_time;
1668 $hold->fulfillment_time('now');
1669 $hold->fulfillment_staff($e->requestor->id);
1670 $hold->fulfillment_lib($self->circ_lib);
1672 return $self->bail_on_events($e->event)
1673 unless $e->update_action_hold_request($hold);
1675 $holdcode->delete_hold_copy_maps($e, $hold->id);
1676 return $self->fulfilled_holds([$hold->id]);
1680 # ------------------------------------------------------------------------------
1681 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1682 # the patron directly targets the checked out item, see if there is another hold
1683 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1684 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1685 # ------------------------------------------------------------------------------
1686 sub find_related_user_hold {
1687 my($self, $copy, $patron) = @_;
1688 my $e = $self->editor;
1690 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1692 return undef unless $U->ou_ancestor_setting_value(
1693 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1695 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1697 select => {ahr => ['id']},
1702 fkey => 'current_copy',
1703 type => 'left' # there may be no current_copy
1710 fulfillment_time => undef,
1711 cancel_time => undef,
1713 {expire_time => undef},
1714 {expire_time => {'>' => 'now'}}
1721 target => $self->volume->id
1727 target => $self->title->id
1733 {id => undef}, # left-join copy may be nonexistent
1734 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1738 order_by => {ahr => {request_time => {direction => 'asc'}}},
1742 my $hold_info = $e->json_query($args)->[0];
1743 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1748 sub run_checkout_scripts {
1753 my $runner = $self->script_runner;
1762 my $hard_due_date_name;
1764 if(!$self->legacy_script_support) {
1765 $self->run_indb_circ_test();
1766 $duration = $self->circ_matrix_matchpoint->duration_rule;
1767 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1768 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1769 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1773 $runner->load($self->circ_duration);
1775 my $result = $runner->run or
1776 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1778 $duration_name = $result->{durationRule};
1779 $recurring_name = $result->{recurringFinesRule};
1780 $max_fine_name = $result->{maxFine};
1781 $hard_due_date_name = $result->{hardDueDate};
1784 $duration_name = $duration->name if $duration;
1785 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1788 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1789 return $self->bail_on_events($evt) if ($evt && !$nobail);
1791 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1792 return $self->bail_on_events($evt) if ($evt && !$nobail);
1794 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1795 return $self->bail_on_events($evt) if ($evt && !$nobail);
1797 if($hard_due_date_name) {
1798 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1799 return $self->bail_on_events($evt) if ($evt && !$nobail);
1805 # The item circulates with an unlimited duration
1809 $hard_due_date = undef;
1812 $self->duration_rule($duration);
1813 $self->recurring_fines_rule($recurring);
1814 $self->max_fine_rule($max_fine);
1815 $self->hard_due_date($hard_due_date);
1819 sub build_checkout_circ_object {
1822 my $circ = Fieldmapper::action::circulation->new;
1823 my $duration = $self->duration_rule;
1824 my $max = $self->max_fine_rule;
1825 my $recurring = $self->recurring_fines_rule;
1826 my $hard_due_date = $self->hard_due_date;
1827 my $copy = $self->copy;
1828 my $patron = $self->patron;
1829 my $duration_date_ceiling;
1830 my $duration_date_ceiling_force;
1834 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1835 $duration_date_ceiling = $policy->{duration_date_ceiling};
1836 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1838 my $dname = $duration->name;
1839 my $mname = $max->name;
1840 my $rname = $recurring->name;
1842 if($hard_due_date) {
1843 $hdname = $hard_due_date->name;
1846 $logger->debug("circulator: building circulation ".
1847 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1849 $circ->duration($policy->{duration});
1850 $circ->recurring_fine($policy->{recurring_fine});
1851 $circ->duration_rule($duration->name);
1852 $circ->recurring_fine_rule($recurring->name);
1853 $circ->max_fine_rule($max->name);
1854 $circ->max_fine($policy->{max_fine});
1855 $circ->fine_interval($recurring->recurrence_interval);
1856 $circ->renewal_remaining($duration->max_renewals);
1857 $circ->grace_period($policy->{grace_period});
1861 $logger->info("circulator: copy found with an unlimited circ duration");
1862 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1863 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1864 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1865 $circ->renewal_remaining(0);
1866 $circ->grace_period(0);
1869 $circ->target_copy( $copy->id );
1870 $circ->usr( $patron->id );
1871 $circ->circ_lib( $self->circ_lib );
1872 $circ->workstation($self->editor->requestor->wsid)
1873 if defined $self->editor->requestor->wsid;
1875 # renewals maintain a link to the parent circulation
1876 $circ->parent_circ($self->parent_circ);
1878 if( $self->is_renewal ) {
1879 $circ->opac_renewal('t') if $self->opac_renewal;
1880 $circ->phone_renewal('t') if $self->phone_renewal;
1881 $circ->desk_renewal('t') if $self->desk_renewal;
1882 $circ->renewal_remaining($self->renewal_remaining);
1883 $circ->circ_staff($self->editor->requestor->id);
1887 # if the user provided an overiding checkout time,
1888 # (e.g. the checkout really happened several hours ago), then
1889 # we apply that here. Does this need a perm??
1890 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1891 if $self->checkout_time;
1893 # if a patron is renewing, 'requestor' will be the patron
1894 $circ->circ_staff($self->editor->requestor->id);
1895 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1900 sub do_reservation_pickup {
1903 $self->log_me("do_reservation_pickup()");
1905 $self->reservation->pickup_time('now');
1908 $self->reservation->current_resource &&
1909 $U->is_true($self->reservation->target_resource_type->catalog_item)
1911 # We used to try to set $self->copy and $self->patron here,
1912 # but that should already be done.
1914 $self->run_checkout_scripts(1);
1916 my $duration = $self->duration_rule;
1917 my $max = $self->max_fine_rule;
1918 my $recurring = $self->recurring_fines_rule;
1920 if ($duration && $max && $recurring) {
1921 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1923 my $dname = $duration->name;
1924 my $mname = $max->name;
1925 my $rname = $recurring->name;
1927 $logger->debug("circulator: updating reservation ".
1928 "with duration=$dname, maxfine=$mname, recurring=$rname");
1930 $self->reservation->fine_amount($policy->{recurring_fine});
1931 $self->reservation->max_fine($policy->{max_fine});
1932 $self->reservation->fine_interval($recurring->recurrence_interval);
1935 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1936 $self->update_copy();
1939 $self->reservation->fine_amount(
1940 $self->reservation->target_resource_type->fine_amount
1942 $self->reservation->max_fine(
1943 $self->reservation->target_resource_type->max_fine
1945 $self->reservation->fine_interval(
1946 $self->reservation->target_resource_type->fine_interval
1950 $self->update_reservation();
1953 sub do_reservation_return {
1955 my $request = shift;
1957 $self->log_me("do_reservation_return()");
1959 if (not ref $self->reservation) {
1960 my ($reservation, $evt) =
1961 $U->fetch_booking_reservation($self->reservation);
1962 return $self->bail_on_events($evt) if $evt;
1963 $self->reservation($reservation);
1966 $self->generate_fines(1);
1967 $self->reservation->return_time('now');
1968 $self->update_reservation();
1969 $self->reshelve_copy if $self->copy;
1971 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1972 $self->copy( $self->reservation->current_resource->catalog_item );
1976 sub booking_adjusted_due_date {
1978 my $circ = $self->circ;
1979 my $copy = $self->copy;
1981 return undef unless $self->use_booking;
1985 if( $self->due_date ) {
1987 return $self->bail_on_events($self->editor->event)
1988 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1990 $circ->due_date(cleanse_ISO8601($self->due_date));
1994 return unless $copy and $circ->due_date;
1997 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1998 if (@$booking_items) {
1999 my $booking_item = $booking_items->[0];
2000 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2002 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2003 my $shorten_circ_setting = $resource_type->elbow_room ||
2004 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2007 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2008 my $bookings = $booking_ses->request(
2009 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2010 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
2012 $booking_ses->disconnect;
2014 my $dt_parser = DateTime::Format::ISO8601->new;
2015 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2017 for my $bid (@$bookings) {
2019 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2021 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2022 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2024 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2025 if ($booking_start < DateTime->now);
2028 if ($U->is_true($stop_circ_setting)) {
2029 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2031 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2032 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2035 # We set the circ duration here only to affect the logic that will
2036 # later (in a DB trigger) mangle the time part of the due date to
2037 # 11:59pm. Having any circ duration that is not a whole number of
2038 # days is enough to prevent the "correction."
2039 my $new_circ_duration = $due_date->epoch - time;
2040 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2041 $circ->duration("$new_circ_duration seconds");
2043 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2047 return $self->bail_on_events($self->editor->event)
2048 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2054 sub apply_modified_due_date {
2056 my $shift_earlier = shift;
2057 my $circ = $self->circ;
2058 my $copy = $self->copy;
2060 if( $self->due_date ) {
2062 return $self->bail_on_events($self->editor->event)
2063 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2065 $circ->due_date(cleanse_ISO8601($self->due_date));
2069 # if the due_date lands on a day when the location is closed
2070 return unless $copy and $circ->due_date;
2072 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2074 # due-date overlap should be determined by the location the item
2075 # is checked out from, not the owning or circ lib of the item
2076 my $org = $self->circ_lib;
2078 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2079 " with an item due date of ".$circ->due_date );
2081 my $dateinfo = $U->storagereq(
2082 'open-ils.storage.actor.org_unit.closed_date.overlap',
2083 $org, $circ->due_date );
2086 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2087 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2089 # XXX make the behavior more dynamic
2090 # for now, we just push the due date to after the close date
2091 if ($shift_earlier) {
2092 $circ->due_date($dateinfo->{start});
2094 $circ->due_date($dateinfo->{end});
2102 sub create_due_date {
2103 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2105 # if there is a raw time component (e.g. from postgres),
2106 # turn it into an interval that interval_to_seconds can parse
2107 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2109 # for now, use the server timezone. TODO: use workstation org timezone
2110 my $due_date = DateTime->now(time_zone => 'local');
2112 # add the circ duration
2113 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2116 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2117 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2118 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2123 # return ISO8601 time with timezone
2124 return $due_date->strftime('%FT%T%z');
2129 sub make_precat_copy {
2131 my $copy = $self->copy;
2134 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2136 $copy->editor($self->editor->requestor->id);
2137 $copy->edit_date('now');
2138 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2139 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2140 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2141 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2142 $self->update_copy();
2146 $logger->info("circulator: Creating a new precataloged ".
2147 "copy in checkout with barcode " . $self->copy_barcode);
2149 $copy = Fieldmapper::asset::copy->new;
2150 $copy->circ_lib($self->circ_lib);
2151 $copy->creator($self->editor->requestor->id);
2152 $copy->editor($self->editor->requestor->id);
2153 $copy->barcode($self->copy_barcode);
2154 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2155 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2156 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2158 $copy->dummy_title($self->dummy_title || "");
2159 $copy->dummy_author($self->dummy_author || "");
2160 $copy->dummy_isbn($self->dummy_isbn || "");
2161 $copy->circ_modifier($self->circ_modifier);
2164 # See if we need to override the circ_lib for the copy with a configured circ_lib
2165 # Setting is shortname of the org unit
2166 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2167 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2169 if($precat_circ_lib) {
2170 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2173 $self->bail_on_events($self->editor->event);
2177 $copy->circ_lib($org->id);
2181 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2183 $self->push_events($self->editor->event);
2187 # this is a little bit of a hack, but we need to
2188 # get the copy into the script runner
2189 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2193 sub checkout_noncat {
2199 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2200 my $count = $self->noncat_count || 1;
2201 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2203 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2207 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2208 $self->editor->requestor->id,
2216 $self->push_events($evt);
2224 # If a copy goes into transit and is then checked in before the transit checkin
2225 # interval has expired, push an event onto the overridable events list.
2226 sub check_transit_checkin_interval {
2229 # only concerned with in-transit items
2230 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2232 # no interval, no problem
2233 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2234 return unless $interval;
2236 # capture the transit so we don't have to fetch it again later during checkin
2238 $self->editor->search_action_transit_copy(
2239 {target_copy => $self->copy->id, dest_recv_time => undef}
2243 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2244 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2245 my $horizon = $t_start->add(seconds => $seconds);
2247 # See if we are still within the transit checkin forbidden range
2248 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2249 if $horizon > DateTime->now;
2255 $self->log_me("do_checkin()");
2257 return $self->bail_on_events(
2258 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2261 $self->check_transit_checkin_interval;
2263 # the renew code and mk_env should have already found our circulation object
2264 unless( $self->circ ) {
2266 my $circs = $self->editor->search_action_circulation(
2267 { target_copy => $self->copy->id, checkin_time => undef });
2269 $self->circ($$circs[0]);
2271 # for now, just warn if there are multiple open circs on a copy
2272 $logger->warn("circulator: we have ".scalar(@$circs).
2273 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2276 # run the fine generator against this circ, if this circ is there
2277 $self->generate_fines_start if $self->circ;
2279 if( $self->checkin_check_holds_shelf() ) {
2280 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2281 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2282 $self->checkin_flesh_events;
2286 unless( $self->is_renewal ) {
2287 return $self->bail_on_events($self->editor->event)
2288 unless $self->editor->allowed('COPY_CHECKIN');
2291 $self->push_events($self->check_copy_alert());
2292 $self->push_events($self->check_checkin_copy_status());
2294 # if the circ is marked as 'claims returned', add the event to the list
2295 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2296 if ($self->circ and $self->circ->stop_fines
2297 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2299 $self->check_circ_deposit();
2301 # handle the overridable events
2302 $self->override_events unless $self->is_renewal;
2303 return if $self->bail_out;
2305 if( $self->copy and !$self->transit ) {
2307 $self->editor->search_action_transit_copy(
2308 { target_copy => $self->copy->id, dest_recv_time => undef }
2314 $self->generate_fines_finish;
2315 $self->checkin_handle_circ;
2316 return if $self->bail_out;
2317 $self->checkin_changed(1);
2319 } elsif( $self->transit ) {
2320 my $hold_transit = $self->process_received_transit;
2321 $self->checkin_changed(1);
2323 if( $self->bail_out ) {
2324 $self->checkin_flesh_events;
2328 if( my $e = $self->check_checkin_copy_status() ) {
2329 # If the original copy status is special, alert the caller
2330 my $ev = $self->events;
2331 $self->events([$e]);
2332 $self->override_events;
2333 return if $self->bail_out;
2337 if( $hold_transit or
2338 $U->copy_status($self->copy->status)->id
2339 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2342 if( $hold_transit ) {
2343 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2345 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2350 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2352 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2353 $self->reshelve_copy(1);
2354 $self->cancelled_hold_transit(1);
2355 $self->notify_hold(0); # don't notify for cancelled holds
2356 return if $self->bail_out;
2360 # hold transited to correct location
2361 $self->checkin_flesh_events;
2366 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2368 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2369 " that is in-transit, but there is no transit.. repairing");
2370 $self->reshelve_copy(1);
2371 return if $self->bail_out;
2374 if( $self->is_renewal ) {
2375 $self->finish_fines_and_voiding;
2376 return if $self->bail_out;
2377 $self->push_events(OpenILS::Event->new('SUCCESS'));
2381 # ------------------------------------------------------------------------------
2382 # Circulations and transits are now closed where necessary. Now go on to see if
2383 # this copy can fulfill a hold or needs to be routed to a different location
2384 # ------------------------------------------------------------------------------
2386 my $needed_for_something = 0; # formerly "needed_for_hold"
2388 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2390 if (!$self->remote_hold) {
2391 if ($self->use_booking) {
2392 my $potential_hold = $self->hold_capture_is_possible;
2393 my $potential_reservation = $self->reservation_capture_is_possible;
2395 if ($potential_hold and $potential_reservation) {
2396 $logger->info("circulator: item could fulfill either hold or reservation");
2397 $self->push_events(new OpenILS::Event(
2398 "HOLD_RESERVATION_CONFLICT",
2399 "hold" => $potential_hold,
2400 "reservation" => $potential_reservation
2402 return if $self->bail_out;
2403 } elsif ($potential_hold) {
2404 $needed_for_something =
2405 $self->attempt_checkin_hold_capture;
2406 } elsif ($potential_reservation) {
2407 $needed_for_something =
2408 $self->attempt_checkin_reservation_capture;
2411 $needed_for_something = $self->attempt_checkin_hold_capture;
2414 return if $self->bail_out;
2416 unless($needed_for_something) {
2417 my $circ_lib = (ref $self->copy->circ_lib) ?
2418 $self->copy->circ_lib->id : $self->copy->circ_lib;
2420 if( $self->remote_hold ) {
2421 $circ_lib = $self->remote_hold->pickup_lib;
2422 $logger->warn("circulator: Copy ".$self->copy->barcode.
2423 " is on a remote hold's shelf, sending to $circ_lib");
2426 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2428 if( $circ_lib == $self->circ_lib) {
2429 # copy is where it needs to be, either for hold or reshelving
2431 $self->checkin_handle_precat();
2432 return if $self->bail_out;
2435 # copy needs to transit "home", or stick here if it's a floating copy
2437 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2438 $self->checkin_changed(1);
2439 $self->copy->circ_lib( $self->circ_lib );
2442 my $bc = $self->copy->barcode;
2443 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2444 $self->checkin_build_copy_transit($circ_lib);
2445 return if $self->bail_out;
2446 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2450 } else { # no-op checkin
2451 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2452 $self->checkin_changed(1);
2453 $self->copy->circ_lib( $self->circ_lib );
2458 if($self->claims_never_checked_out and
2459 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2461 # the item was not supposed to be checked out to the user and should now be marked as missing
2462 $self->copy->status(OILS_COPY_STATUS_MISSING);
2466 $self->reshelve_copy unless $needed_for_something;
2469 return if $self->bail_out;
2471 unless($self->checkin_changed) {
2473 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2474 my $stat = $U->copy_status($self->copy->status)->id;
2476 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2477 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2478 $self->bail_out(1); # no need to commit anything
2482 $self->push_events(OpenILS::Event->new('SUCCESS'))
2483 unless @{$self->events};
2486 $self->finish_fines_and_voiding;
2488 OpenILS::Utils::Penalty->calculate_penalties(
2489 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2491 $self->checkin_flesh_events;
2495 sub finish_fines_and_voiding {
2497 return unless $self->circ;
2499 # gather any updates to the circ after fine generation, if there was a circ
2500 $self->generate_fines_finish;
2502 return unless $self->backdate or $self->void_overdues;
2504 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2505 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2507 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2508 $self->editor, $self->circ, $self->backdate, $note);
2510 return $self->bail_on_events($evt) if $evt;
2512 # make sure the circ isn't closed if we just voided some fines
2513 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2514 return $self->bail_on_events($evt) if $evt;
2520 # if a deposit was payed for this item, push the event
2521 sub check_circ_deposit {
2523 return unless $self->circ;
2524 my $deposit = $self->editor->search_money_billing(
2526 xact => $self->circ->id,
2528 }, {idlist => 1})->[0];
2530 $self->push_events(OpenILS::Event->new(
2531 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2536 my $force = $self->force || shift;
2537 my $copy = $self->copy;
2539 my $stat = $U->copy_status($copy->status)->id;
2542 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2543 $stat != OILS_COPY_STATUS_CATALOGING and
2544 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2545 $stat != OILS_COPY_STATUS_RESHELVING )) {
2547 $copy->status( OILS_COPY_STATUS_RESHELVING );
2549 $self->checkin_changed(1);
2554 # Returns true if the item is at the current location
2555 # because it was transited there for a hold and the
2556 # hold has not been fulfilled
2557 sub checkin_check_holds_shelf {
2559 return 0 unless $self->copy;
2562 $U->copy_status($self->copy->status)->id ==
2563 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2565 # find the hold that put us on the holds shelf
2566 my $holds = $self->editor->search_action_hold_request(
2568 current_copy => $self->copy->id,
2569 capture_time => { '!=' => undef },
2570 fulfillment_time => undef,
2571 cancel_time => undef,
2576 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2577 $self->reshelve_copy(1);
2581 my $hold = $$holds[0];
2583 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2584 $hold->id. "] for copy ".$self->copy->barcode);
2586 if( $hold->pickup_lib == $self->circ_lib ) {
2587 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2591 $logger->info("circulator: hold is not for here..");
2592 $self->remote_hold($hold);
2597 sub checkin_handle_precat {
2599 my $copy = $self->copy;
2601 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2602 $copy->status(OILS_COPY_STATUS_CATALOGING);
2603 $self->update_copy();
2604 $self->checkin_changed(1);
2605 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2610 sub checkin_build_copy_transit {
2613 my $copy = $self->copy;
2614 my $transit = Fieldmapper::action::transit_copy->new;
2616 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2617 $logger->info("circulator: transiting copy to $dest");
2619 $transit->source($self->circ_lib);
2620 $transit->dest($dest);
2621 $transit->target_copy($copy->id);
2622 $transit->source_send_time('now');
2623 $transit->copy_status( $U->copy_status($copy->status)->id );
2625 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2627 return $self->bail_on_events($self->editor->event)
2628 unless $self->editor->create_action_transit_copy($transit);
2630 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2632 $self->checkin_changed(1);
2636 sub hold_capture_is_possible {
2638 my $copy = $self->copy;
2640 # we've been explicitly told not to capture any holds
2641 return 0 if $self->capture eq 'nocapture';
2643 # See if this copy can fulfill any holds
2644 my $hold = $holdcode->find_nearest_permitted_hold(
2645 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2647 return undef if ref $hold eq "HASH" and
2648 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2652 sub reservation_capture_is_possible {
2654 my $copy = $self->copy;
2656 # we've been explicitly told not to capture any holds
2657 return 0 if $self->capture eq 'nocapture';
2659 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2660 my $resv = $booking_ses->request(
2661 "open-ils.booking.reservations.could_capture",
2662 $self->editor->authtoken, $copy->barcode
2664 $booking_ses->disconnect;
2665 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2666 $self->push_events($resv);
2672 # returns true if the item was used (or may potentially be used
2673 # in subsequent calls) to capture a hold.
2674 sub attempt_checkin_hold_capture {
2676 my $copy = $self->copy;
2678 # we've been explicitly told not to capture any holds
2679 return 0 if $self->capture eq 'nocapture';
2681 # See if this copy can fulfill any holds
2682 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2683 $self->editor, $copy, $self->editor->requestor );
2686 $logger->debug("circulator: no potential permitted".
2687 "holds found for copy ".$copy->barcode);
2691 if($self->capture ne 'capture') {
2692 # see if this item is in a hold-capture-delay location
2693 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2694 if($U->is_true($location->hold_verify)) {
2695 $self->bail_on_events(
2696 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2701 $self->retarget($retarget);
2703 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2705 $hold->current_copy($copy->id);
2706 $hold->capture_time('now');
2707 $self->put_hold_on_shelf($hold)
2708 if $hold->pickup_lib == $self->circ_lib;
2710 # prevent DB errors caused by fetching
2711 # holds from storage, and updating through cstore
2712 $hold->clear_fulfillment_time;
2713 $hold->clear_fulfillment_staff;
2714 $hold->clear_fulfillment_lib;
2715 $hold->clear_expire_time;
2716 $hold->clear_cancel_time;
2717 $hold->clear_prev_check_time unless $hold->prev_check_time;
2719 $self->bail_on_events($self->editor->event)
2720 unless $self->editor->update_action_hold_request($hold);
2722 $self->checkin_changed(1);
2724 return 0 if $self->bail_out;
2726 if( $hold->pickup_lib == $self->circ_lib ) {
2728 # This hold was captured in the correct location
2729 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2730 $self->push_events(OpenILS::Event->new('SUCCESS'));
2732 #$self->do_hold_notify($hold->id);
2733 $self->notify_hold($hold->id);
2737 # Hold needs to be picked up elsewhere. Build a hold
2738 # transit and route the item.
2739 $self->checkin_build_hold_transit();
2740 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2741 return 0 if $self->bail_out;
2742 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2745 # make sure we save the copy status
2750 sub attempt_checkin_reservation_capture {
2752 my $copy = $self->copy;
2754 # we've been explicitly told not to capture any holds
2755 return 0 if $self->capture eq 'nocapture';
2757 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2758 my $evt = $booking_ses->request(
2759 "open-ils.booking.resources.capture_for_reservation",
2760 $self->editor->authtoken,
2762 1 # don't update copy - we probably have it locked
2764 $booking_ses->disconnect;
2766 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2768 "open-ils.booking.resources.capture_for_reservation " .
2769 "didn't return an event!"
2773 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2774 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2776 # not-transferable is an error event we'll pass on the user
2777 $logger->warn("reservation capture attempted against non-transferable item");
2778 $self->push_events($evt);
2780 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2781 # Re-retrieve copy as reservation capture may have changed
2782 # its status and whatnot.
2784 "circulator: booking capture win on copy " . $self->copy->id
2786 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2788 "circulator: changing copy " . $self->copy->id .
2789 "'s status from " . $self->copy->status . " to " .
2792 $self->copy->status($new_copy_status);
2795 $self->reservation($evt->{"payload"}->{"reservation"});
2797 if (exists $evt->{"payload"}->{"transit"}) {
2801 "org" => $evt->{"payload"}->{"transit"}->dest
2805 $self->checkin_changed(1);
2809 # other results are treated as "nothing to capture"
2813 sub do_hold_notify {
2814 my( $self, $holdid ) = @_;
2816 my $e = new_editor(xact => 1);
2817 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2819 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2820 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2822 $logger->info("circulator: running delayed hold notify process");
2824 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2825 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2827 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2828 hold_id => $holdid, requestor => $self->editor->requestor);
2830 $logger->debug("circulator: built hold notifier");
2832 if(!$notifier->event) {
2834 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2836 my $stat = $notifier->send_email_notify;
2837 if( $stat == '1' ) {
2838 $logger->info("circulator: hold notify succeeded for hold $holdid");
2842 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2845 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2849 sub retarget_holds {
2851 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2852 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2853 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2854 # no reason to wait for the return value
2858 sub checkin_build_hold_transit {
2861 my $copy = $self->copy;
2862 my $hold = $self->hold;
2863 my $trans = Fieldmapper::action::hold_transit_copy->new;
2865 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2867 $trans->hold($hold->id);
2868 $trans->source($self->circ_lib);
2869 $trans->dest($hold->pickup_lib);
2870 $trans->source_send_time("now");
2871 $trans->target_copy($copy->id);
2873 # when the copy gets to its destination, it will recover
2874 # this status - put it onto the holds shelf
2875 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2877 return $self->bail_on_events($self->editor->event)
2878 unless $self->editor->create_action_hold_transit_copy($trans);
2883 sub process_received_transit {
2885 my $copy = $self->copy;
2886 my $copyid = $self->copy->id;
2888 my $status_name = $U->copy_status($copy->status)->name;
2889 $logger->debug("circulator: attempting transit receive on ".
2890 "copy $copyid. Copy status is $status_name");
2892 my $transit = $self->transit;
2894 if( $transit->dest != $self->circ_lib ) {
2895 # - this item is in-transit to a different location
2897 my $tid = $transit->id;
2898 my $loc = $self->circ_lib;
2899 my $dest = $transit->dest;
2901 $logger->info("circulator: Fowarding transit on copy which is destined ".
2902 "for a different location. transit=$tid, copy=$copyid, current ".
2903 "location=$loc, destination location=$dest");
2905 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2907 # grab the associated hold object if available
2908 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2909 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2911 return $self->bail_on_events($evt);
2914 # The transit is received, set the receive time
2915 $transit->dest_recv_time('now');
2916 $self->bail_on_events($self->editor->event)
2917 unless $self->editor->update_action_transit_copy($transit);
2919 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2921 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2922 $copy->status( $transit->copy_status );
2923 $self->update_copy();
2924 return if $self->bail_out;
2928 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2930 # hold has arrived at destination, set shelf time
2931 $self->put_hold_on_shelf($hold);
2932 $self->bail_on_events($self->editor->event)
2933 unless $self->editor->update_action_hold_request($hold);
2934 return if $self->bail_out;
2936 $self->notify_hold($hold_transit->hold);
2941 OpenILS::Event->new(
2944 payload => { transit => $transit, holdtransit => $hold_transit } ));
2946 return $hold_transit;
2950 # ------------------------------------------------------------------
2951 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2952 # ------------------------------------------------------------------
2953 sub put_hold_on_shelf {
2954 my($self, $hold) = @_;
2956 $hold->shelf_time('now');
2958 my $shelf_expire = $U->ou_ancestor_setting_value(
2959 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2961 return undef unless $shelf_expire;
2963 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2964 my $expire_time = DateTime->now->add(seconds => $seconds);
2966 # if the shelf expire time overlaps with a pickup lib's
2967 # closed date, push it out to the first open date
2968 my $dateinfo = $U->storagereq(
2969 'open-ils.storage.actor.org_unit.closed_date.overlap',
2970 $hold->pickup_lib, $expire_time);
2973 my $dt_parser = DateTime::Format::ISO8601->new;
2974 $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
2976 # TODO: enable/disable time bump via setting?
2977 $expire_time->set(hour => '23', minute => '59', second => '59');
2979 $logger->info("circulator: shelf_expire_time overlaps".
2980 " with closed date, pushing expire time to $expire_time");
2983 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2989 sub generate_fines {
2991 my $reservation = shift;
2993 $self->generate_fines_start($reservation);
2994 $self->generate_fines_finish($reservation);
2999 sub generate_fines_start {
3001 my $reservation = shift;
3002 my $dt_parser = DateTime::Format::ISO8601->new;
3004 my $obj = $reservation ? $self->reservation : $self->circ;
3006 # If we have a grace period
3007 if($obj->can('grace_period')) {
3008 # Parse out the due date
3009 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3010 # Add the grace period to the due date
3011 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3012 # Don't generate fines on circs still in grace period
3013 return undef if ($due_date > DateTime->now);
3016 if (!exists($self->{_gen_fines_req})) {
3017 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3019 'open-ils.storage.action.circulation.overdue.generate_fines',
3027 sub generate_fines_finish {
3029 my $reservation = shift;
3031 return undef unless $self->{_gen_fines_req};
3033 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3035 $self->{_gen_fines_req}->wait_complete;
3036 delete($self->{_gen_fines_req});
3038 # refresh the circ in case the fine generator set the stop_fines field
3039 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3040 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3045 sub checkin_handle_circ {
3047 my $circ = $self->circ;
3048 my $copy = $self->copy;
3052 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3054 # backdate the circ if necessary
3055 if($self->backdate) {
3056 my $evt = $self->checkin_handle_backdate;
3057 return $self->bail_on_events($evt) if $evt;
3060 if(!$circ->stop_fines) {
3061 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3062 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3063 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3064 $circ->stop_fines_time('now');
3065 $circ->stop_fines_time($self->backdate) if $self->backdate;
3068 # Set the checkin vars since we have the item
3069 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3071 # capture the true scan time for back-dated checkins
3072 $circ->checkin_scan_time('now');
3074 $circ->checkin_staff($self->editor->requestor->id);
3075 $circ->checkin_lib($self->circ_lib);
3076 $circ->checkin_workstation($self->editor->requestor->wsid);
3078 my $circ_lib = (ref $self->copy->circ_lib) ?
3079 $self->copy->circ_lib->id : $self->copy->circ_lib;
3080 my $stat = $U->copy_status($self->copy->status)->id;
3082 # immediately available keeps items lost or missing items from going home before being handled
3083 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3084 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3087 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3089 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3090 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3092 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3096 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3098 $self->checkin_handle_lost($circ_lib);
3102 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3107 # see if there are any fines owed on this circ. if not, close it
3108 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3109 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3111 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3113 return $self->bail_on_events($self->editor->event)
3114 unless $self->editor->update_action_circulation($circ);
3120 # ------------------------------------------------------------------
3121 # See if we need to void billings for lost checkin
3122 # ------------------------------------------------------------------
3123 sub checkin_handle_lost {
3125 my $circ_lib = shift;
3126 my $circ = $self->circ;
3128 my $max_return = $U->ou_ancestor_setting_value(
3129 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3134 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3135 $tm[5] -= 1 if $tm[5] > 0;
3136 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3138 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3139 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3141 $max_return = 0 if $today < $last_chance;
3144 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3146 my $void_lost = $U->ou_ancestor_setting_value(
3147 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3148 my $void_lost_fee = $U->ou_ancestor_setting_value(
3149 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3150 my $restore_od = $U->ou_ancestor_setting_value(
3151 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3152 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3153 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3155 $self->checkin_handle_lost_now_found(3) if $void_lost;
3156 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3157 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3160 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3165 sub checkin_handle_backdate {
3168 # ------------------------------------------------------------------
3169 # clean up the backdate for date comparison
3170 # XXX We are currently taking the due-time from the original due-date,
3171 # not the input. Do we need to do this? This certainly interferes with
3172 # backdating of hourly checkouts, but that is likely a very rare case.
3173 # ------------------------------------------------------------------
3174 my $bd = cleanse_ISO8601($self->backdate);
3175 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3176 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3177 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3179 $self->backdate($bd);
3184 sub check_checkin_copy_status {
3186 my $copy = $self->copy;
3188 my $status = $U->copy_status($copy->status)->id;
3191 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3192 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3193 $status == OILS_COPY_STATUS_IN_PROCESS ||
3194 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3195 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3196 $status == OILS_COPY_STATUS_CATALOGING ||
3197 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3198 $status == OILS_COPY_STATUS_RESHELVING );
3200 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3201 if( $status == OILS_COPY_STATUS_LOST );
3203 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3204 if( $status == OILS_COPY_STATUS_MISSING );
3206 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3211 # --------------------------------------------------------------------------
3212 # On checkin, we need to return as many relevant objects as we can
3213 # --------------------------------------------------------------------------
3214 sub checkin_flesh_events {
3217 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3218 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3219 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3222 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3225 if($self->hold and !$self->hold->cancel_time) {
3226 $hold = $self->hold;
3227 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3231 # if we checked in a circulation, flesh the billing summary data
3232 $self->circ->billable_transaction(
3233 $self->editor->retrieve_money_billable_transaction([
3235 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3241 # flesh some patron fields before returning
3243 $self->editor->retrieve_actor_user([
3248 au => ['card', 'billing_address', 'mailing_address']
3255 for my $evt (@{$self->events}) {
3258 $payload->{copy} = $U->unflesh_copy($self->copy);
3259 $payload->{volume} = $self->volume;
3260 $payload->{record} = $record,
3261 $payload->{circ} = $self->circ;
3262 $payload->{transit} = $self->transit;
3263 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3264 $payload->{hold} = $hold;
3265 $payload->{patron} = $self->patron;
3266 $payload->{reservation} = $self->reservation
3267 unless (not $self->reservation or $self->reservation->cancel_time);
3269 $evt->{payload} = $payload;
3274 my( $self, $msg ) = @_;
3275 my $bc = ($self->copy) ? $self->copy->barcode :
3278 my $usr = ($self->patron) ? $self->patron->id : "";
3279 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3280 ", recipient=$usr, copy=$bc");
3286 $self->log_me("do_renew()");
3288 # Make sure there is an open circ to renew that is not
3289 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3290 my $usrid = $self->patron->id if $self->patron;
3291 my $circ = $self->editor->search_action_circulation({
3292 target_copy => $self->copy->id,
3293 xact_finish => undef,
3294 checkin_time => undef,
3295 ($usrid ? (usr => $usrid) : ()),
3297 {stop_fines => undef},
3298 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3302 return $self->bail_on_events($self->editor->event) unless $circ;
3304 # A user is not allowed to renew another user's items without permission
3305 unless( $circ->usr eq $self->editor->requestor->id ) {
3306 return $self->bail_on_events($self->editor->events)
3307 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3310 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3311 if $circ->renewal_remaining < 1;
3313 # -----------------------------------------------------------------
3315 $self->parent_circ($circ->id);
3316 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3319 # Opac renewal - re-use circ library from original circ (unless told not to)
3320 if($self->opac_renewal) {
3321 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3322 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3323 $self->circ_lib($circ->circ_lib);
3327 # Run the fine generator against the old circ
3328 $self->generate_fines_start;
3330 $self->run_renew_permit;
3333 $self->do_checkin();
3334 return if $self->bail_out;
3336 unless( $self->permit_override ) {
3338 return if $self->bail_out;
3339 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3340 $self->remove_event('ITEM_NOT_CATALOGED');
3343 $self->override_events;
3344 return if $self->bail_out;
3347 $self->do_checkout();
3352 my( $self, $evt ) = @_;
3353 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3354 $logger->debug("circulator: removing event from list: $evt");
3355 my @events = @{$self->events};
3356 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3361 my( $self, $evt ) = @_;
3362 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3363 return grep { $_->{textcode} eq $evt } @{$self->events};
3368 sub run_renew_permit {
3371 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3372 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3373 $self->editor, $self->copy, $self->editor->requestor, 1
3375 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3378 if(!$self->legacy_script_support) {
3379 my $results = $self->run_indb_circ_test;
3380 $self->push_events($self->matrix_test_result_events)
3381 unless $self->circ_test_success;
3384 my $runner = $self->script_runner;
3386 $runner->load($self->circ_permit_renew);
3387 my $result = $runner->run or
3388 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3389 if ($result->{"events"}) {
3391 map { new OpenILS::Event($_) } @{$result->{"events"}}
3394 "circulator: circ_permit_renew for user " .
3395 $self->patron->id . " returned " .
3396 scalar(@{$result->{"events"}}) . " event(s)"
3400 $self->mk_script_runner;
3403 $logger->debug("circulator: re-creating script runner to be safe");
3407 # XXX: The primary mechanism for storing circ history is now handled
3408 # by tracking real circulation objects instead of bibs in a bucket.
3409 # However, this code is disabled by default and could be useful
3410 # some day, so may as well leave it for now.
3411 sub append_reading_list {
3415 $self->is_checkout and
3421 # verify history is globally enabled and uses the bucket mechanism
3422 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3423 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3425 return undef unless $htype and $htype eq 'bucket';
3427 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3429 # verify the patron wants to retain the hisory
3430 my $setting = $e->search_actor_user_setting(
3431 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3433 unless($setting and $setting->value) {
3438 my $bkt = $e->search_container_copy_bucket(
3439 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3444 # find the next item position
3445 my $last_item = $e->search_container_copy_bucket_item(
3446 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3447 $pos = $last_item->pos + 1 if $last_item;
3450 # create the history bucket if necessary
3451 $bkt = Fieldmapper::container::copy_bucket->new;
3452 $bkt->owner($self->patron->id);
3454 $bkt->btype('circ_history');
3456 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3459 my $item = Fieldmapper::container::copy_bucket_item->new;
3461 $item->bucket($bkt->id);
3462 $item->target_copy($self->copy->id);
3465 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3472 sub make_trigger_events {
3474 return unless $self->circ;
3475 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3476 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3477 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3482 sub checkin_handle_lost_now_found {
3483 my ($self, $bill_type) = @_;
3485 # ------------------------------------------------------------------
3486 # remove charge from patron's account if lost item is returned
3487 # ------------------------------------------------------------------
3489 my $bills = $self->editor->search_money_billing(
3491 xact => $self->circ->id,
3496 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3497 for my $bill (@$bills) {
3498 if( !$U->is_true($bill->voided) ) {
3499 $logger->info("lost item returned - voiding bill ".$bill->id);
3501 $bill->void_time('now');
3502 $bill->voider($self->editor->requestor->id);
3503 my $note = ($bill->note) ? $bill->note . "\n" : '';
3504 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3506 $self->bail_on_events($self->editor->event)
3507 unless $self->editor->update_money_billing($bill);
3512 sub checkin_handle_lost_now_found_restore_od {
3514 my $circ_lib = shift;
3516 # ------------------------------------------------------------------
3517 # restore those overdue charges voided when item was set to lost
3518 # ------------------------------------------------------------------
3520 my $ods = $self->editor->search_money_billing(
3522 xact => $self->circ->id,
3527 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3528 for my $bill (@$ods) {
3529 if( $U->is_true($bill->voided) ) {
3530 $logger->info("lost item returned - restoring overdue ".$bill->id);
3532 $bill->clear_void_time;
3533 $bill->voider($self->editor->requestor->id);
3534 my $note = ($bill->note) ? $bill->note . "\n" : '';
3535 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3537 $self->bail_on_events($self->editor->event)
3538 unless $self->editor->update_money_billing($bill);
3543 # ------------------------------------------------------------------
3544 # Lost-then-found item checked in. This sub generates new overdue
3545 # fines, beyond the point of any existing and possibly voided
3546 # overdue fines, up to the point of final checkin time (or max fine
3548 # ------------------------------------------------------------------
3549 sub generate_lost_overdue_fines {
3551 my $circ = $self->circ;
3552 my $e = $self->editor;
3554 # Re-open the transaction so the fine generator can see it
3555 if($circ->xact_finish or $circ->stop_fines) {
3557 $circ->clear_xact_finish;
3558 $circ->clear_stop_fines;
3559 $circ->clear_stop_fines_time;
3560 $e->update_action_circulation($circ) or return $e->die_event;
3564 $e->xact_begin; # generate_fines expects an in-xact editor
3565 $self->generate_fines;
3566 $circ = $self->circ; # generate fines re-fetches the circ
3570 # Re-close the transaction if no money is owed
3571 my ($obt) = $U->fetch_mbts($circ->id, $e);
3572 if ($obt and $obt->balance_owed == 0) {
3573 $circ->xact_finish('now');
3577 # Set stop fines if the fine generator didn't have to
3578 unless($circ->stop_fines) {
3579 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3580 $circ->stop_fines_time('now');
3584 # update the event data sent to the caller within the transaction
3585 $self->checkin_flesh_events;
3588 $e->update_action_circulation($circ) or return $e->die_event;