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;
17 my $opac_renewal_use_circ_lib;
19 sub determine_booking_status {
20 unless (defined $booking_status) {
21 my $ses = create OpenSRF::AppSession("router");
22 $booking_status = grep {$_ eq "open-ils.booking"} @{
23 $ses->request("opensrf.router.info.class.list")->gather(1)
26 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
29 return $booking_status;
35 flesh_fields => {acp => ['call_number','parts'], acn => ['record']}
41 my $conf = OpenSRF::Utils::SettingsClient->new;
42 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
44 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
45 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
47 my $lb = $conf->config_value( @pfx2, 'script_path' );
48 $lb = [ $lb ] unless ref($lb);
51 return unless $legacy_script_support;
53 my @pfx = ( @pfx2, "scripts" );
54 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
55 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
56 my $d = $conf->config_value( @pfx, 'circ_duration' );
57 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
58 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
59 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
61 $logger->error( "Missing circ script(s)" )
62 unless( $p and $c and $d and $f and $m and $pr );
64 $scripts{circ_permit_patron} = $p;
65 $scripts{circ_permit_copy} = $c;
66 $scripts{circ_duration} = $d;
67 $scripts{circ_recurring_fines} = $f;
68 $scripts{circ_max_fines} = $m;
69 $scripts{circ_permit_renew} = $pr;
72 "circulator: Loaded rules scripts for circ: " .
73 "circ permit patron = $p, ".
74 "circ permit copy = $c, ".
75 "circ duration = $d, ".
76 "circ recurring fines = $f, " .
77 "circ max fines = $m, ".
78 "circ renew permit = $pr. ".
80 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
84 __PACKAGE__->register_method(
85 method => "run_method",
86 api_name => "open-ils.circ.checkout.permit",
88 Determines if the given checkout can occur
89 @param authtoken The login session key
90 @param params A trailing hash of named params including
91 barcode : The copy barcode,
92 patron : The patron the checkout is occurring for,
93 renew : true or false - whether or not this is a renewal
94 @return The event that occurred during the permit check.
98 __PACKAGE__->register_method (
99 method => 'run_method',
100 api_name => 'open-ils.circ.checkout.permit.override',
101 signature => q/@see open-ils.circ.checkout.permit/,
105 __PACKAGE__->register_method(
106 method => "run_method",
107 api_name => "open-ils.circ.checkout",
110 @param authtoken The login session key
111 @param params A named hash of params including:
113 barcode If no copy is provided, the copy is retrieved via barcode
114 copyid If no copy or barcode is provide, the copy id will be use
115 patron The patron's id
116 noncat True if this is a circulation for a non-cataloted item
117 noncat_type The non-cataloged type id
118 noncat_circ_lib The location for the noncat circ.
119 precat The item has yet to be cataloged
120 dummy_title The temporary title of the pre-cataloded item
121 dummy_author The temporary authr of the pre-cataloded item
122 Default is the home org of the staff member
123 @return The SUCCESS event on success, any other event depending on the error
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.checkin",
131 Generic super-method for handling all copies
132 @param authtoken The login session key
133 @param params Hash of named parameters including:
134 barcode - The copy barcode
135 force - If true, copies in bad statuses will be checked in and give good statuses
136 noop - don't capture holds or put items into transit
137 void_overdues - void all overdues for the circulation (aka amnesty)
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkin.override",
145 signature => q/@see open-ils.circ.checkin/
148 __PACKAGE__->register_method(
149 method => "run_method",
150 api_name => "open-ils.circ.renew.override",
151 signature => q/@see open-ils.circ.renew/,
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.renew",
158 notes => <<" NOTES");
159 PARAMS( authtoken, circ => circ_id );
160 open-ils.circ.renew(login_session, circ_object);
161 Renews the provided circulation. login_session is the requestor of the
162 renewal and if the logged in user is not the same as circ->usr, then
163 the logged in user must have RENEW_CIRC permissions.
166 __PACKAGE__->register_method(
167 method => "run_method",
168 api_name => "open-ils.circ.checkout.full"
170 __PACKAGE__->register_method(
171 method => "run_method",
172 api_name => "open-ils.circ.checkout.full.override"
174 __PACKAGE__->register_method(
175 method => "run_method",
176 api_name => "open-ils.circ.reservation.pickup"
178 __PACKAGE__->register_method(
179 method => "run_method",
180 api_name => "open-ils.circ.reservation.return"
182 __PACKAGE__->register_method(
183 method => "run_method",
184 api_name => "open-ils.circ.reservation.return.override"
186 __PACKAGE__->register_method(
187 method => "run_method",
188 api_name => "open-ils.circ.checkout.inspect",
189 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
194 my( $self, $conn, $auth, $args ) = @_;
195 translate_legacy_args($args);
196 my $api = $self->api_name;
199 OpenILS::Application::Circ::Circulator->new($auth, %$args);
201 return circ_events($circulator) if $circulator->bail_out;
203 $circulator->use_booking(determine_booking_status());
205 # --------------------------------------------------------------------------
206 # First, check for a booking transit, as the barcode may not be a copy
207 # barcode, but a resource barcode, and nothing else in here will work
208 # --------------------------------------------------------------------------
210 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
211 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
212 if (@$resources) { # yes!
214 my $res_id_list = [ map { $_->id } @$resources ];
215 my $transit = $circulator->editor->search_action_reservation_transit_copy(
217 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
218 { order_by => { artc => 'source_send_time' }, limit => 1 }
220 )->[0]; # Any transit for this barcode?
222 if ($transit) { # yes! unwrap it.
224 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
225 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
227 my $success_event = new OpenILS::Event(
228 "SUCCESS", "payload" => {"reservation" => $reservation}
230 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
231 if (my $copy = $circulator->editor->search_asset_copy([
232 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
233 ])->[0]) { # got a copy
234 $copy->status( $transit->copy_status );
235 $copy->editor($circulator->editor->requestor->id);
236 $copy->edit_date('now');
237 $circulator->editor->update_asset_copy($copy);
238 $success_event->{"payload"}->{"record"} =
239 $U->record_to_mvr($copy->call_number->record);
240 $success_event->{"payload"}->{"volume"} = $copy->call_number;
241 $copy->call_number($copy->call_number->id);
242 $success_event->{"payload"}->{"copy"} = $copy;
246 $transit->dest_recv_time('now');
247 $circulator->editor->update_action_reservation_transit_copy( $transit );
249 $circulator->editor->commit;
250 # Formerly this branch just stopped here. Argh!
251 $conn->respond_complete($success_event);
259 # --------------------------------------------------------------------------
260 # Go ahead and load the script runner to make sure we have all
261 # of the objects we need
262 # --------------------------------------------------------------------------
264 if ($circulator->use_booking) {
265 $circulator->is_res_checkin($circulator->is_checkin(1))
266 if $api =~ /reservation.return/ or (
267 $api =~ /checkin/ and $circulator->seems_like_reservation()
270 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
273 $circulator->is_renewal(1) if $api =~ /renew/;
274 $circulator->is_checkin(1) if $api =~ /checkin/;
276 $circulator->mk_env();
277 $circulator->noop(1) if $circulator->claims_never_checked_out;
279 if($legacy_script_support and not $circulator->is_checkin) {
280 $circulator->mk_script_runner();
281 $circulator->legacy_script_support(1);
282 $circulator->circ_permit_patron($scripts{circ_permit_patron});
283 $circulator->circ_permit_copy($scripts{circ_permit_copy});
284 $circulator->circ_duration($scripts{circ_duration});
285 $circulator->circ_permit_renew($scripts{circ_permit_renew});
287 return circ_events($circulator) if $circulator->bail_out;
290 $circulator->override(1) if $api =~ /override/o;
292 if( $api =~ /checkout\.permit/ ) {
293 $circulator->do_permit();
295 } elsif( $api =~ /checkout.full/ ) {
297 # requesting a precat checkout implies that any required
298 # overrides have been performed. Go ahead and re-override.
299 $circulator->skip_permit_key(1);
300 $circulator->override(1) if $circulator->request_precat;
301 $circulator->do_permit();
302 $circulator->is_checkout(1);
303 unless( $circulator->bail_out ) {
304 $circulator->events([]);
305 $circulator->do_checkout();
308 } elsif( $circulator->is_res_checkout ) {
309 $circulator->do_reservation_pickup();
311 } elsif( $api =~ /inspect/ ) {
312 my $data = $circulator->do_inspect();
313 $circulator->editor->rollback;
316 } elsif( $api =~ /checkout/ ) {
317 $circulator->is_checkout(1);
318 $circulator->do_checkout();
320 } elsif( $circulator->is_res_checkin ) {
321 $circulator->do_reservation_return();
322 $circulator->do_checkin() if ($circulator->copy());
323 } elsif( $api =~ /checkin/ ) {
324 $circulator->do_checkin();
326 } elsif( $api =~ /renew/ ) {
327 $circulator->is_renewal(1);
328 $circulator->do_renew();
331 if( $circulator->bail_out ) {
334 # make sure no success event accidentally slip in
336 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
339 my @e = @{$circulator->events};
340 push( @ee, $_->{textcode} ) for @e;
341 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
343 $circulator->editor->rollback;
347 $circulator->editor->commit;
349 if ($circulator->generate_lost_overdue) {
350 # Generating additional overdue billings has to happen after the
351 # main commit and before the final respond() so the caller can
352 # receive the latest transaction summary.
353 my $evt = $circulator->generate_lost_overdue_fines;
354 $circulator->bail_on_events($evt) if $evt;
358 $conn->respond_complete(circ_events($circulator));
360 $circulator->script_runner->cleanup if $circulator->script_runner;
362 return undef if $circulator->bail_out;
364 $circulator->do_hold_notify($circulator->notify_hold)
365 if $circulator->notify_hold;
366 $circulator->retarget_holds if $circulator->retarget;
367 $circulator->append_reading_list;
368 $circulator->make_trigger_events;
375 my @e = @{$circ->events};
376 # if we have multiple events, SUCCESS should not be one of them;
377 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
378 return (@e == 1) ? $e[0] : \@e;
382 sub translate_legacy_args {
385 if( $$args{barcode} ) {
386 $$args{copy_barcode} = $$args{barcode};
387 delete $$args{barcode};
390 if( $$args{copyid} ) {
391 $$args{copy_id} = $$args{copyid};
392 delete $$args{copyid};
395 if( $$args{patronid} ) {
396 $$args{patron_id} = $$args{patronid};
397 delete $$args{patronid};
400 if( $$args{patron} and !ref($$args{patron}) ) {
401 $$args{patron_id} = $$args{patron};
402 delete $$args{patron};
406 if( $$args{noncat} ) {
407 $$args{is_noncat} = $$args{noncat};
408 delete $$args{noncat};
411 if( $$args{precat} ) {
412 $$args{is_precat} = $$args{request_precat} = $$args{precat};
413 delete $$args{precat};
419 # --------------------------------------------------------------------------
420 # This package actually manages all of the circulation logic
421 # --------------------------------------------------------------------------
422 package OpenILS::Application::Circ::Circulator;
423 use strict; use warnings;
424 use vars q/$AUTOLOAD/;
426 use OpenILS::Utils::Fieldmapper;
427 use OpenSRF::Utils::Cache;
428 use Digest::MD5 qw(md5_hex);
429 use DateTime::Format::ISO8601;
430 use OpenILS::Utils::PermitHold;
431 use OpenSRF::Utils qw/:datetime/;
432 use OpenSRF::Utils::SettingsClient;
433 use OpenILS::Application::Circ::Holds;
434 use OpenILS::Application::Circ::Transit;
435 use OpenSRF::Utils::Logger qw(:logger);
436 use OpenILS::Utils::CStoreEditor qw/:funcs/;
437 use OpenILS::Application::Circ::ScriptBuilder;
438 use OpenILS::Const qw/:const/;
439 use OpenILS::Utils::Penalty;
440 use OpenILS::Application::Circ::CircCommon;
443 my $holdcode = "OpenILS::Application::Circ::Holds";
444 my $transcode = "OpenILS::Application::Circ::Transit";
450 # --------------------------------------------------------------------------
451 # Add a pile of automagic getter/setter methods
452 # --------------------------------------------------------------------------
453 my @AUTOLOAD_FIELDS = qw/
500 recurring_fines_level
513 cancelled_hold_transit
520 circ_matrix_matchpoint
522 legacy_script_support
532 claims_never_checked_out
537 generate_lost_overdue
543 my $type = ref($self) or die "$self is not an object";
545 my $name = $AUTOLOAD;
548 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
549 $logger->error("circulator: $type: invalid autoload field: $name");
550 die "$type: invalid autoload field: $name\n"
555 *{"${type}::${name}"} = sub {
558 $s->{$name} = $v if defined $v;
562 return $self->$name($data);
567 my( $class, $auth, %args ) = @_;
568 $class = ref($class) || $class;
569 my $self = bless( {}, $class );
572 $self->editor(new_editor(xact => 1, authtoken => $auth));
574 unless( $self->editor->checkauth ) {
575 $self->bail_on_events($self->editor->event);
579 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
581 $self->$_($args{$_}) for keys %args;
584 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
586 # if this is a renewal, default to desk_renewal
587 $self->desk_renewal(1) unless
588 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
590 $self->capture('') unless $self->capture;
592 unless(%user_groups) {
593 my $gps = $self->editor->retrieve_all_permission_grp_tree;
594 %user_groups = map { $_->id => $_ } @$gps;
601 # --------------------------------------------------------------------------
602 # True if we should discontinue processing
603 # --------------------------------------------------------------------------
605 my( $self, $bool ) = @_;
606 if( defined $bool ) {
607 $logger->info("circulator: BAILING OUT") if $bool;
608 $self->{bail_out} = $bool;
610 return $self->{bail_out};
615 my( $self, @evts ) = @_;
618 $e->{payload} = $self->copy if
619 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
621 $logger->info("circulator: pushing event ".$e->{textcode});
622 push( @{$self->events}, $e ) unless
623 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
629 return '' if $self->skip_permit_key;
630 my $key = md5_hex( time() . rand() . "$$" );
631 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
632 return $self->permit_key($key);
635 sub check_permit_key {
637 return 1 if $self->skip_permit_key;
638 my $key = $self->permit_key;
639 return 0 unless $key;
640 my $k = "oils_permit_key_$key";
641 my $one = $self->cache_handle->get_cache($k);
642 $self->cache_handle->delete_cache($k);
643 return ($one) ? 1 : 0;
646 sub seems_like_reservation {
649 # Some words about the following method:
650 # 1) It requires the VIEW_USER permission, but that's not an
651 # issue, right, since all staff should have that?
652 # 2) It returns only one reservation at a time, even if an item can be
653 # and is currently overbooked. Hmmm....
654 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
655 my $result = $booking_ses->request(
656 "open-ils.booking.reservations.by_returnable_resource_barcode",
657 $self->editor->authtoken,
660 $booking_ses->disconnect;
662 return $self->bail_on_events($result) if defined $U->event_code($result);
665 $self->reservation(shift @$result);
673 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
674 sub save_trimmed_copy {
675 my ($self, $copy) = @_;
678 $self->volume($copy->call_number);
679 $self->title($self->volume->record);
680 $self->copy->call_number($self->volume->id);
681 $self->volume->record($self->title->id);
682 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
683 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
684 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
685 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
691 my $e = $self->editor;
693 # --------------------------------------------------------------------------
694 # Grab the fleshed copy
695 # --------------------------------------------------------------------------
696 unless($self->is_noncat) {
699 $copy = $e->retrieve_asset_copy(
700 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
702 } elsif( $self->copy_barcode ) {
704 $copy = $e->search_asset_copy(
705 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
706 } elsif( $self->reservation ) {
707 my $res = $e->json_query(
709 "select" => {"acp" => ["id"]},
714 "field" => "barcode",
718 "field" => "current_resource"
726 "id" => (ref $self->reservation) ?
727 $self->reservation->id : $self->reservation
732 if (ref $res eq "ARRAY" and scalar @$res) {
733 $logger->info("circulator: mapped reservation " .
734 $self->reservation . " to copy " . $res->[0]->{"id"});
735 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
740 $self->save_trimmed_copy($copy);
742 # We can't renew if there is no copy
743 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
744 if $self->is_renewal;
749 # --------------------------------------------------------------------------
751 # --------------------------------------------------------------------------
755 flesh_fields => {au => [ qw/ card / ]}
758 if( $self->patron_id ) {
759 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
760 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
762 } elsif( $self->patron_barcode ) {
764 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
765 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
766 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
768 $patron = $e->retrieve_actor_user($card->usr)
769 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
771 # Use the card we looked up, not the patron's primary, for card active checks
772 $patron->card($card);
775 if( my $copy = $self->copy ) {
778 $flesh->{flesh_fields}->{circ} = ['usr'];
780 my $circ = $e->search_action_circulation([
781 {target_copy => $copy->id, checkin_time => undef}, $flesh
785 $patron = $circ->usr;
786 $circ->usr($patron->id); # de-flesh for consistency
792 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
793 unless $self->patron($patron) or $self->is_checkin;
795 unless($self->is_checkin) {
797 # Check for inactivity and patron reg. expiration
799 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
800 unless $U->is_true($patron->active);
802 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
803 unless $U->is_true($patron->card->active);
805 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
806 cleanse_ISO8601($patron->expire_date));
808 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
809 if( CORE::time > $expire->epoch ) ;
813 # --------------------------------------------------------------------------
814 # This builds the script runner environment and fetches most of the
816 # --------------------------------------------------------------------------
817 sub mk_script_runner {
823 qw/copy copy_barcode copy_id patron
824 patron_id patron_barcode volume title editor/;
826 # Translate our objects into the ScriptBuilder args hash
827 $$args{$_} = $self->$_() for @fields;
829 $args->{ignore_user_status} = 1 if $self->is_checkin;
830 $$args{fetch_patron_by_circ_copy} = 1;
831 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
833 if( my $pco = $self->pending_checkouts ) {
834 $logger->info("circulator: we were given a pending checkouts number of $pco");
835 $$args{patronItemsOut} = $pco;
838 # This fetches most of the objects we need
839 $self->script_runner(
840 OpenILS::Application::Circ::ScriptBuilder->build($args));
842 # Now we translate the ScriptBuilder objects back into self
843 $self->$_($$args{$_}) for @fields;
845 my @evts = @{$args->{_events}} if $args->{_events};
847 $logger->debug("circulator: script builder returned events: @evts") if @evts;
851 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
852 if(!$self->is_noncat and
854 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
858 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
859 return $self->bail_on_events(@e);
864 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
865 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
866 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
867 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
871 # We can't renew if there is no copy
872 return $self->bail_on_events(@evts) if
873 $self->is_renewal and !$self->copy;
875 # Set some circ-specific flags in the script environment
876 my $evt = "environment";
877 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
879 if( $self->is_noncat ) {
880 $self->script_runner->insert("$evt.isNonCat", 1);
881 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
884 if( $self->is_precat ) {
885 $self->script_runner->insert("environment.isPrecat", 1, 1);
888 $self->script_runner->add_path( $_ ) for @$script_libs;
893 # --------------------------------------------------------------------------
894 # Does the circ permit work
895 # --------------------------------------------------------------------------
899 $self->log_me("do_permit()");
901 unless( $self->editor->requestor->id == $self->patron->id ) {
902 return $self->bail_on_events($self->editor->event)
903 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
906 $self->check_captured_holds();
907 $self->do_copy_checks();
908 return if $self->bail_out;
909 $self->run_patron_permit_scripts();
910 $self->run_copy_permit_scripts()
911 unless $self->is_precat or $self->is_noncat;
912 $self->check_item_deposit_events();
913 $self->override_events();
914 return if $self->bail_out;
916 if($self->is_precat and not $self->request_precat) {
919 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
920 return $self->bail_out(1) unless $self->is_renewal;
924 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
927 sub check_item_deposit_events {
929 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
930 if $self->is_deposit and not $self->is_deposit_exempt;
931 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
932 if $self->is_rental and not $self->is_rental_exempt;
935 # returns true if the user is not required to pay deposits
936 sub is_deposit_exempt {
938 my $pid = (ref $self->patron->profile) ?
939 $self->patron->profile->id : $self->patron->profile;
940 my $groups = $U->ou_ancestor_setting_value(
941 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
942 for my $grp (@$groups) {
943 return 1 if $self->is_group_descendant($grp, $pid);
948 # returns true if the user is not required to pay rental fees
949 sub is_rental_exempt {
951 my $pid = (ref $self->patron->profile) ?
952 $self->patron->profile->id : $self->patron->profile;
953 my $groups = $U->ou_ancestor_setting_value(
954 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
955 for my $grp (@$groups) {
956 return 1 if $self->is_group_descendant($grp, $pid);
961 sub is_group_descendant {
962 my($self, $p_id, $c_id) = @_;
963 return 0 unless defined $p_id and defined $c_id;
964 return 1 if $c_id == $p_id;
965 while(my $grp = $user_groups{$c_id}) {
966 $c_id = $grp->parent;
967 return 0 unless defined $c_id;
968 return 1 if $c_id == $p_id;
973 sub check_captured_holds {
975 my $copy = $self->copy;
976 my $patron = $self->patron;
978 return undef unless $copy;
980 my $s = $U->copy_status($copy->status)->id;
981 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
982 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
984 # Item is on the holds shelf, make sure it's going to the right person
985 my $holds = $self->editor->search_action_hold_request(
988 current_copy => $copy->id ,
989 capture_time => { '!=' => undef },
990 cancel_time => undef,
991 fulfillment_time => undef
997 if( $holds and $$holds[0] ) {
998 return undef if $$holds[0]->usr == $patron->id;
1001 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1003 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1007 sub do_copy_checks {
1009 my $copy = $self->copy;
1010 return unless $copy;
1012 my $stat = $U->copy_status($copy->status)->id;
1014 # We cannot check out a copy if it is in-transit
1015 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1016 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1019 $self->handle_claims_returned();
1020 return if $self->bail_out;
1022 # no claims returned circ was found, check if there is any open circ
1023 unless( $self->is_renewal ) {
1025 my $circs = $self->editor->search_action_circulation(
1026 { target_copy => $copy->id, checkin_time => undef }
1029 if(my $old_circ = $circs->[0]) { # an open circ was found
1031 my $payload = {copy => $copy};
1033 if($old_circ->usr == $self->patron->id) {
1035 $payload->{old_circ} = $old_circ;
1037 # If there is an open circulation on the checkout item and an auto-renew
1038 # interval is defined, inform the caller that they should go
1039 # ahead and renew the item instead of warning about open circulations.
1041 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1043 'circ.checkout_auto_renew_age',
1047 if($auto_renew_intvl) {
1048 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1049 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1051 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1052 $payload->{auto_renew} = 1;
1057 return $self->bail_on_events(
1058 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1064 my $LEGACY_CIRC_EVENT_MAP = {
1065 'no_item' => 'ITEM_NOT_CATALOGED',
1066 'actor.usr.barred' => 'PATRON_BARRED',
1067 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1068 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1069 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1070 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1071 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1072 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1073 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1074 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1078 # ---------------------------------------------------------------------
1079 # This pushes any patron-related events into the list but does not
1080 # set bail_out for any events
1081 # ---------------------------------------------------------------------
1082 sub run_patron_permit_scripts {
1084 my $runner = $self->script_runner;
1085 my $patronid = $self->patron->id;
1089 if(!$self->legacy_script_support) {
1091 my $results = $self->run_indb_circ_test;
1092 unless($self->circ_test_success) {
1093 # no_item result is OK during noncat checkout
1094 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1095 push @allevents, $self->matrix_test_result_events;
1101 # ---------------------------------------------------------------------
1102 # # Now run the patron permit script
1103 # ---------------------------------------------------------------------
1104 $runner->load($self->circ_permit_patron);
1105 my $result = $runner->run or
1106 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1108 my $patron_events = $result->{events};
1110 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1111 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1112 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1113 $penalties = $penalties->{fatal_penalties};
1115 for my $pen (@$penalties) {
1116 my $event = OpenILS::Event->new($pen->name);
1117 $event->{desc} = $pen->label;
1118 push(@allevents, $event);
1121 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1125 $_->{payload} = $self->copy if
1126 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1129 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1131 $self->push_events(@allevents);
1134 sub matrix_test_result_codes {
1136 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1139 sub matrix_test_result_events {
1142 my $event = new OpenILS::Event(
1143 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1145 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1147 } (@{$self->matrix_test_result});
1150 sub run_indb_circ_test {
1152 return $self->matrix_test_result if $self->matrix_test_result;
1154 my $dbfunc = ($self->is_renewal) ?
1155 'action.item_user_renew_test' : 'action.item_user_circ_test';
1157 if( $self->is_precat && $self->request_precat) {
1158 $self->make_precat_copy;
1159 return if $self->bail_out;
1162 my $results = $self->editor->json_query(
1166 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1172 $self->circ_test_success($U->is_true($results->[0]->{success}));
1174 if(my $mp = $results->[0]->{matchpoint}) {
1175 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1176 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1177 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1178 if(defined($results->[0]->{renewals})) {
1179 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1181 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1182 if(defined($results->[0]->{grace_period})) {
1183 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1185 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1186 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1189 return $self->matrix_test_result($results);
1192 # ---------------------------------------------------------------------
1193 # given a use and copy, this will calculate the circulation policy
1194 # parameters. Only works with in-db circ.
1195 # ---------------------------------------------------------------------
1199 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1201 $self->run_indb_circ_test;
1204 circ_test_success => $self->circ_test_success,
1205 failure_events => [],
1206 failure_codes => [],
1207 matchpoint => $self->circ_matrix_matchpoint
1210 unless($self->circ_test_success) {
1211 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1212 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1215 if($self->circ_matrix_matchpoint) {
1216 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1217 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1218 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1219 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1221 my $policy = $self->get_circ_policy(
1222 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1224 $$results{$_} = $$policy{$_} for keys %$policy;
1230 # ---------------------------------------------------------------------
1231 # Loads the circ policy info for duration, recurring fine, and max
1232 # fine based on the current copy
1233 # ---------------------------------------------------------------------
1234 sub get_circ_policy {
1235 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1238 duration_rule => $duration_rule->name,
1239 recurring_fine_rule => $recurring_fine_rule->name,
1240 max_fine_rule => $max_fine_rule->name,
1241 max_fine => $self->get_max_fine_amount($max_fine_rule),
1242 fine_interval => $recurring_fine_rule->recurrence_interval,
1243 renewal_remaining => $duration_rule->max_renewals,
1244 grace_period => $recurring_fine_rule->grace_period
1247 if($hard_due_date) {
1248 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1249 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1252 $policy->{duration_date_ceiling} = undef;
1253 $policy->{duration_date_ceiling_force} = undef;
1256 $policy->{duration} = $duration_rule->shrt
1257 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1258 $policy->{duration} = $duration_rule->normal
1259 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1260 $policy->{duration} = $duration_rule->extended
1261 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1263 $policy->{recurring_fine} = $recurring_fine_rule->low
1264 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1265 $policy->{recurring_fine} = $recurring_fine_rule->normal
1266 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1267 $policy->{recurring_fine} = $recurring_fine_rule->high
1268 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1273 sub get_max_fine_amount {
1275 my $max_fine_rule = shift;
1276 my $max_amount = $max_fine_rule->amount;
1278 # if is_percent is true then the max->amount is
1279 # use as a percentage of the copy price
1280 if ($U->is_true($max_fine_rule->is_percent)) {
1281 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1282 $max_amount = $price * $max_fine_rule->amount / 100;
1284 $U->ou_ancestor_setting_value(
1286 'circ.max_fine.cap_at_price',
1290 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1291 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1299 sub run_copy_permit_scripts {
1301 my $copy = $self->copy || return;
1302 my $runner = $self->script_runner;
1306 if(!$self->legacy_script_support) {
1307 my $results = $self->run_indb_circ_test;
1308 push @allevents, $self->matrix_test_result_events
1309 unless $self->circ_test_success;
1312 # ---------------------------------------------------------------------
1313 # Capture all of the copy permit events
1314 # ---------------------------------------------------------------------
1315 $runner->load($self->circ_permit_copy);
1316 my $result = $runner->run or
1317 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1318 my $copy_events = $result->{events};
1320 # ---------------------------------------------------------------------
1321 # Now collect all of the events together
1322 # ---------------------------------------------------------------------
1323 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1326 # See if this copy has an alert message
1327 my $ae = $self->check_copy_alert();
1328 push( @allevents, $ae ) if $ae;
1330 # uniquify the events
1331 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1332 @allevents = values %hash;
1334 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1336 $self->push_events(@allevents);
1340 sub check_copy_alert {
1342 return undef if $self->is_renewal;
1343 return OpenILS::Event->new(
1344 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1345 if $self->copy and $self->copy->alert_message;
1351 # --------------------------------------------------------------------------
1352 # If the call is overriding and has permissions to override every collected
1353 # event, the are cleared. Any event that the caller does not have
1354 # permission to override, will be left in the event list and bail_out will
1356 # XXX We need code in here to cancel any holds/transits on copies
1357 # that are being force-checked out
1358 # --------------------------------------------------------------------------
1359 sub override_events {
1361 my @events = @{$self->events};
1362 return unless @events;
1364 if(!$self->override) {
1365 return $self->bail_out(1)
1366 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1371 for my $e (@events) {
1372 my $tc = $e->{textcode};
1373 next if $tc eq 'SUCCESS';
1374 my $ov = "$tc.override";
1375 $logger->info("circulator: attempting to override event: $ov");
1377 return $self->bail_on_events($self->editor->event)
1378 unless( $self->editor->allowed($ov) );
1383 # --------------------------------------------------------------------------
1384 # If there is an open claimsreturn circ on the requested copy, close the
1385 # circ if overriding, otherwise bail out
1386 # --------------------------------------------------------------------------
1387 sub handle_claims_returned {
1389 my $copy = $self->copy;
1391 my $CR = $self->editor->search_action_circulation(
1393 target_copy => $copy->id,
1394 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1395 checkin_time => undef,
1399 return unless ($CR = $CR->[0]);
1403 # - If the caller has set the override flag, we will check the item in
1404 if($self->override) {
1406 $CR->checkin_time('now');
1407 $CR->checkin_scan_time('now');
1408 $CR->checkin_lib($self->circ_lib);
1409 $CR->checkin_workstation($self->editor->requestor->wsid);
1410 $CR->checkin_staff($self->editor->requestor->id);
1412 $evt = $self->editor->event
1413 unless $self->editor->update_action_circulation($CR);
1416 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1419 $self->bail_on_events($evt) if $evt;
1424 # --------------------------------------------------------------------------
1425 # This performs the checkout
1426 # --------------------------------------------------------------------------
1430 $self->log_me("do_checkout()");
1432 # make sure perms are good if this isn't a renewal
1433 unless( $self->is_renewal ) {
1434 return $self->bail_on_events($self->editor->event)
1435 unless( $self->editor->allowed('COPY_CHECKOUT') );
1438 # verify the permit key
1439 unless( $self->check_permit_key ) {
1440 if( $self->permit_override ) {
1441 return $self->bail_on_events($self->editor->event)
1442 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1444 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1448 # if this is a non-cataloged circ, build the circ and finish
1449 if( $self->is_noncat ) {
1450 $self->checkout_noncat;
1452 OpenILS::Event->new('SUCCESS',
1453 payload => { noncat_circ => $self->circ }));
1457 if( $self->is_precat ) {
1458 $self->make_precat_copy;
1459 return if $self->bail_out;
1461 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1462 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1465 $self->do_copy_checks;
1466 return if $self->bail_out;
1468 $self->run_checkout_scripts();
1469 return if $self->bail_out;
1471 $self->build_checkout_circ_object();
1472 return if $self->bail_out;
1474 my $modify_to_start = $self->booking_adjusted_due_date();
1475 return if $self->bail_out;
1477 $self->apply_modified_due_date($modify_to_start);
1478 return if $self->bail_out;
1480 return $self->bail_on_events($self->editor->event)
1481 unless $self->editor->create_action_circulation($self->circ);
1483 # refresh the circ to force local time zone for now
1484 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1486 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1488 return if $self->bail_out;
1490 $self->apply_deposit_fee();
1491 return if $self->bail_out;
1493 $self->handle_checkout_holds();
1494 return if $self->bail_out;
1496 # ------------------------------------------------------------------------------
1497 # Update the patron penalty info in the DB. Run it for permit-overrides
1498 # since the penalties are not updated during the permit phase
1499 # ------------------------------------------------------------------------------
1500 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1502 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1505 if($self->is_renewal) {
1506 # flesh the billing summary for the checked-in circ
1507 $pcirc = $self->editor->retrieve_action_circulation([
1509 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1514 OpenILS::Event->new('SUCCESS',
1516 copy => $U->unflesh_copy($self->copy),
1517 volume => $self->volume,
1518 circ => $self->circ,
1520 holds_fulfilled => $self->fulfilled_holds,
1521 deposit_billing => $self->deposit_billing,
1522 rental_billing => $self->rental_billing,
1523 parent_circ => $pcirc,
1524 patron => ($self->return_patron) ? $self->patron : undef,
1525 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1531 sub apply_deposit_fee {
1533 my $copy = $self->copy;
1535 ($self->is_deposit and not $self->is_deposit_exempt) or
1536 ($self->is_rental and not $self->is_rental_exempt);
1538 return if $self->is_deposit and $self->skip_deposit_fee;
1539 return if $self->is_rental and $self->skip_rental_fee;
1541 my $bill = Fieldmapper::money::billing->new;
1542 my $amount = $copy->deposit_amount;
1546 if($self->is_deposit) {
1547 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1549 $self->deposit_billing($bill);
1551 $billing_type = OILS_BILLING_TYPE_RENTAL;
1553 $self->rental_billing($bill);
1556 $bill->xact($self->circ->id);
1557 $bill->amount($amount);
1558 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1559 $bill->billing_type($billing_type);
1560 $bill->btype($btype);
1561 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1563 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1568 my $copy = $self->copy;
1570 my $stat = $copy->status if ref $copy->status;
1571 my $loc = $copy->location if ref $copy->location;
1572 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1574 $copy->status($stat->id) if $stat;
1575 $copy->location($loc->id) if $loc;
1576 $copy->circ_lib($circ_lib->id) if $circ_lib;
1577 $copy->editor($self->editor->requestor->id);
1578 $copy->edit_date('now');
1579 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1581 return $self->bail_on_events($self->editor->event)
1582 unless $self->editor->update_asset_copy($self->copy);
1584 $copy->status($U->copy_status($copy->status));
1585 $copy->location($loc) if $loc;
1586 $copy->circ_lib($circ_lib) if $circ_lib;
1589 sub update_reservation {
1591 my $reservation = $self->reservation;
1593 my $usr = $reservation->usr;
1594 my $target_rt = $reservation->target_resource_type;
1595 my $target_r = $reservation->target_resource;
1596 my $current_r = $reservation->current_resource;
1598 $reservation->usr($usr->id) if ref $usr;
1599 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1600 $reservation->target_resource($target_r->id) if ref $target_r;
1601 $reservation->current_resource($current_r->id) if ref $current_r;
1603 return $self->bail_on_events($self->editor->event)
1604 unless $self->editor->update_booking_reservation($self->reservation);
1607 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1608 $self->reservation($reservation);
1612 sub bail_on_events {
1613 my( $self, @evts ) = @_;
1614 $self->push_events(@evts);
1619 # ------------------------------------------------------------------------------
1620 # When an item is checked out, see if we can fulfill a hold for this patron
1621 # ------------------------------------------------------------------------------
1622 sub handle_checkout_holds {
1624 my $copy = $self->copy;
1625 my $patron = $self->patron;
1627 my $e = $self->editor;
1628 $self->fulfilled_holds([]);
1630 # pre/non-cats can't fulfill a hold
1631 return if $self->is_precat or $self->is_noncat;
1633 my $hold = $e->search_action_hold_request({
1634 current_copy => $copy->id ,
1635 cancel_time => undef,
1636 fulfillment_time => undef,
1638 {expire_time => undef},
1639 {expire_time => {'>' => 'now'}}
1643 if($hold and $hold->usr != $patron->id) {
1644 # reset the hold since the copy is now checked out
1646 $logger->info("circulator: un-targeting hold ".$hold->id.
1647 " because copy ".$copy->id." is getting checked out");
1649 $hold->clear_prev_check_time;
1650 $hold->clear_current_copy;
1651 $hold->clear_capture_time;
1653 return $self->bail_on_event($e->event)
1654 unless $e->update_action_hold_request($hold);
1660 $hold = $self->find_related_user_hold($copy, $patron) or return;
1661 $logger->info("circulator: found related hold to fulfill in checkout");
1664 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1666 # if the hold was never officially captured, capture it.
1667 $hold->current_copy($copy->id);
1668 $hold->capture_time('now') unless $hold->capture_time;
1669 $hold->fulfillment_time('now');
1670 $hold->fulfillment_staff($e->requestor->id);
1671 $hold->fulfillment_lib($self->circ_lib);
1673 return $self->bail_on_events($e->event)
1674 unless $e->update_action_hold_request($hold);
1676 $holdcode->delete_hold_copy_maps($e, $hold->id);
1677 return $self->fulfilled_holds([$hold->id]);
1681 # ------------------------------------------------------------------------------
1682 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1683 # the patron directly targets the checked out item, see if there is another hold
1684 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1685 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1686 # ------------------------------------------------------------------------------
1687 sub find_related_user_hold {
1688 my($self, $copy, $patron) = @_;
1689 my $e = $self->editor;
1691 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1693 return undef unless $U->ou_ancestor_setting_value(
1694 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1696 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1698 select => {ahr => ['id']},
1703 fkey => 'current_copy',
1704 type => 'left' # there may be no current_copy
1711 fulfillment_time => undef,
1712 cancel_time => undef,
1714 {expire_time => undef},
1715 {expire_time => {'>' => 'now'}}
1722 target => $self->volume->id
1728 target => $self->title->id
1734 {id => undef}, # left-join copy may be nonexistent
1735 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1739 order_by => {ahr => {request_time => {direction => 'asc'}}},
1743 my $hold_info = $e->json_query($args)->[0];
1744 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1749 sub run_checkout_scripts {
1754 my $runner = $self->script_runner;
1763 my $hard_due_date_name;
1765 if(!$self->legacy_script_support) {
1766 $self->run_indb_circ_test();
1767 $duration = $self->circ_matrix_matchpoint->duration_rule;
1768 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1769 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1770 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1774 $runner->load($self->circ_duration);
1776 my $result = $runner->run or
1777 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1779 $duration_name = $result->{durationRule};
1780 $recurring_name = $result->{recurringFinesRule};
1781 $max_fine_name = $result->{maxFine};
1782 $hard_due_date_name = $result->{hardDueDate};
1785 $duration_name = $duration->name if $duration;
1786 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1789 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1790 return $self->bail_on_events($evt) if ($evt && !$nobail);
1792 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1793 return $self->bail_on_events($evt) if ($evt && !$nobail);
1795 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1796 return $self->bail_on_events($evt) if ($evt && !$nobail);
1798 if($hard_due_date_name) {
1799 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1800 return $self->bail_on_events($evt) if ($evt && !$nobail);
1806 # The item circulates with an unlimited duration
1810 $hard_due_date = undef;
1813 $self->duration_rule($duration);
1814 $self->recurring_fines_rule($recurring);
1815 $self->max_fine_rule($max_fine);
1816 $self->hard_due_date($hard_due_date);
1820 sub build_checkout_circ_object {
1823 my $circ = Fieldmapper::action::circulation->new;
1824 my $duration = $self->duration_rule;
1825 my $max = $self->max_fine_rule;
1826 my $recurring = $self->recurring_fines_rule;
1827 my $hard_due_date = $self->hard_due_date;
1828 my $copy = $self->copy;
1829 my $patron = $self->patron;
1830 my $duration_date_ceiling;
1831 my $duration_date_ceiling_force;
1835 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1836 $duration_date_ceiling = $policy->{duration_date_ceiling};
1837 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1839 my $dname = $duration->name;
1840 my $mname = $max->name;
1841 my $rname = $recurring->name;
1843 if($hard_due_date) {
1844 $hdname = $hard_due_date->name;
1847 $logger->debug("circulator: building circulation ".
1848 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1850 $circ->duration($policy->{duration});
1851 $circ->recurring_fine($policy->{recurring_fine});
1852 $circ->duration_rule($duration->name);
1853 $circ->recurring_fine_rule($recurring->name);
1854 $circ->max_fine_rule($max->name);
1855 $circ->max_fine($policy->{max_fine});
1856 $circ->fine_interval($recurring->recurrence_interval);
1857 $circ->renewal_remaining($duration->max_renewals);
1858 $circ->grace_period($policy->{grace_period});
1862 $logger->info("circulator: copy found with an unlimited circ duration");
1863 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1864 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1865 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1866 $circ->renewal_remaining(0);
1867 $circ->grace_period(0);
1870 $circ->target_copy( $copy->id );
1871 $circ->usr( $patron->id );
1872 $circ->circ_lib( $self->circ_lib );
1873 $circ->workstation($self->editor->requestor->wsid)
1874 if defined $self->editor->requestor->wsid;
1876 # renewals maintain a link to the parent circulation
1877 $circ->parent_circ($self->parent_circ);
1879 if( $self->is_renewal ) {
1880 $circ->opac_renewal('t') if $self->opac_renewal;
1881 $circ->phone_renewal('t') if $self->phone_renewal;
1882 $circ->desk_renewal('t') if $self->desk_renewal;
1883 $circ->renewal_remaining($self->renewal_remaining);
1884 $circ->circ_staff($self->editor->requestor->id);
1888 # if the user provided an overiding checkout time,
1889 # (e.g. the checkout really happened several hours ago), then
1890 # we apply that here. Does this need a perm??
1891 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1892 if $self->checkout_time;
1894 # if a patron is renewing, 'requestor' will be the patron
1895 $circ->circ_staff($self->editor->requestor->id);
1896 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
1901 sub do_reservation_pickup {
1904 $self->log_me("do_reservation_pickup()");
1906 $self->reservation->pickup_time('now');
1909 $self->reservation->current_resource &&
1910 $U->is_true($self->reservation->target_resource_type->catalog_item)
1912 # We used to try to set $self->copy and $self->patron here,
1913 # but that should already be done.
1915 $self->run_checkout_scripts(1);
1917 my $duration = $self->duration_rule;
1918 my $max = $self->max_fine_rule;
1919 my $recurring = $self->recurring_fines_rule;
1921 if ($duration && $max && $recurring) {
1922 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1924 my $dname = $duration->name;
1925 my $mname = $max->name;
1926 my $rname = $recurring->name;
1928 $logger->debug("circulator: updating reservation ".
1929 "with duration=$dname, maxfine=$mname, recurring=$rname");
1931 $self->reservation->fine_amount($policy->{recurring_fine});
1932 $self->reservation->max_fine($policy->{max_fine});
1933 $self->reservation->fine_interval($recurring->recurrence_interval);
1936 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1937 $self->update_copy();
1940 $self->reservation->fine_amount(
1941 $self->reservation->target_resource_type->fine_amount
1943 $self->reservation->max_fine(
1944 $self->reservation->target_resource_type->max_fine
1946 $self->reservation->fine_interval(
1947 $self->reservation->target_resource_type->fine_interval
1951 $self->update_reservation();
1954 sub do_reservation_return {
1956 my $request = shift;
1958 $self->log_me("do_reservation_return()");
1960 if (not ref $self->reservation) {
1961 my ($reservation, $evt) =
1962 $U->fetch_booking_reservation($self->reservation);
1963 return $self->bail_on_events($evt) if $evt;
1964 $self->reservation($reservation);
1967 $self->generate_fines(1);
1968 $self->reservation->return_time('now');
1969 $self->update_reservation();
1970 $self->reshelve_copy if $self->copy;
1972 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1973 $self->copy( $self->reservation->current_resource->catalog_item );
1977 sub booking_adjusted_due_date {
1979 my $circ = $self->circ;
1980 my $copy = $self->copy;
1982 return undef unless $self->use_booking;
1986 if( $self->due_date ) {
1988 return $self->bail_on_events($self->editor->event)
1989 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1991 $circ->due_date(cleanse_ISO8601($self->due_date));
1995 return unless $copy and $circ->due_date;
1998 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1999 if (@$booking_items) {
2000 my $booking_item = $booking_items->[0];
2001 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2003 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2004 my $shorten_circ_setting = $resource_type->elbow_room ||
2005 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2008 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2009 my $bookings = $booking_ses->request(
2010 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2011 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
2013 $booking_ses->disconnect;
2015 my $dt_parser = DateTime::Format::ISO8601->new;
2016 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2018 for my $bid (@$bookings) {
2020 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2022 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2023 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2025 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2026 if ($booking_start < DateTime->now);
2029 if ($U->is_true($stop_circ_setting)) {
2030 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2032 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2033 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2036 # We set the circ duration here only to affect the logic that will
2037 # later (in a DB trigger) mangle the time part of the due date to
2038 # 11:59pm. Having any circ duration that is not a whole number of
2039 # days is enough to prevent the "correction."
2040 my $new_circ_duration = $due_date->epoch - time;
2041 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2042 $circ->duration("$new_circ_duration seconds");
2044 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2048 return $self->bail_on_events($self->editor->event)
2049 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2055 sub apply_modified_due_date {
2057 my $shift_earlier = shift;
2058 my $circ = $self->circ;
2059 my $copy = $self->copy;
2061 if( $self->due_date ) {
2063 return $self->bail_on_events($self->editor->event)
2064 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2066 $circ->due_date(cleanse_ISO8601($self->due_date));
2070 # if the due_date lands on a day when the location is closed
2071 return unless $copy and $circ->due_date;
2073 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2075 # due-date overlap should be determined by the location the item
2076 # is checked out from, not the owning or circ lib of the item
2077 my $org = $self->circ_lib;
2079 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2080 " with an item due date of ".$circ->due_date );
2082 my $dateinfo = $U->storagereq(
2083 'open-ils.storage.actor.org_unit.closed_date.overlap',
2084 $org, $circ->due_date );
2087 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2088 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2090 # XXX make the behavior more dynamic
2091 # for now, we just push the due date to after the close date
2092 if ($shift_earlier) {
2093 $circ->due_date($dateinfo->{start});
2095 $circ->due_date($dateinfo->{end});
2103 sub create_due_date {
2104 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2106 # if there is a raw time component (e.g. from postgres),
2107 # turn it into an interval that interval_to_seconds can parse
2108 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2110 # for now, use the server timezone. TODO: use workstation org timezone
2111 my $due_date = DateTime->now(time_zone => 'local');
2113 # add the circ duration
2114 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2117 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2118 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2119 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2124 # return ISO8601 time with timezone
2125 return $due_date->strftime('%FT%T%z');
2130 sub make_precat_copy {
2132 my $copy = $self->copy;
2135 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2137 $copy->editor($self->editor->requestor->id);
2138 $copy->edit_date('now');
2139 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2140 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2141 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2142 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2143 $self->update_copy();
2147 $logger->info("circulator: Creating a new precataloged ".
2148 "copy in checkout with barcode " . $self->copy_barcode);
2150 $copy = Fieldmapper::asset::copy->new;
2151 $copy->circ_lib($self->circ_lib);
2152 $copy->creator($self->editor->requestor->id);
2153 $copy->editor($self->editor->requestor->id);
2154 $copy->barcode($self->copy_barcode);
2155 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2156 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2157 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2159 $copy->dummy_title($self->dummy_title || "");
2160 $copy->dummy_author($self->dummy_author || "");
2161 $copy->dummy_isbn($self->dummy_isbn || "");
2162 $copy->circ_modifier($self->circ_modifier);
2165 # See if we need to override the circ_lib for the copy with a configured circ_lib
2166 # Setting is shortname of the org unit
2167 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2168 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2170 if($precat_circ_lib) {
2171 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2174 $self->bail_on_events($self->editor->event);
2178 $copy->circ_lib($org->id);
2182 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2184 $self->push_events($self->editor->event);
2188 # this is a little bit of a hack, but we need to
2189 # get the copy into the script runner
2190 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2194 sub checkout_noncat {
2200 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2201 my $count = $self->noncat_count || 1;
2202 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2204 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2208 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2209 $self->editor->requestor->id,
2217 $self->push_events($evt);
2225 # If a copy goes into transit and is then checked in before the transit checkin
2226 # interval has expired, push an event onto the overridable events list.
2227 sub check_transit_checkin_interval {
2230 # only concerned with in-transit items
2231 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2233 # no interval, no problem
2234 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2235 return unless $interval;
2237 # capture the transit so we don't have to fetch it again later during checkin
2239 $self->editor->search_action_transit_copy(
2240 {target_copy => $self->copy->id, dest_recv_time => undef}
2244 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2245 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2246 my $horizon = $t_start->add(seconds => $seconds);
2248 # See if we are still within the transit checkin forbidden range
2249 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2250 if $horizon > DateTime->now;
2256 $self->log_me("do_checkin()");
2258 return $self->bail_on_events(
2259 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2262 $self->check_transit_checkin_interval;
2264 # the renew code and mk_env should have already found our circulation object
2265 unless( $self->circ ) {
2267 my $circs = $self->editor->search_action_circulation(
2268 { target_copy => $self->copy->id, checkin_time => undef });
2270 $self->circ($$circs[0]);
2272 # for now, just warn if there are multiple open circs on a copy
2273 $logger->warn("circulator: we have ".scalar(@$circs).
2274 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2277 # run the fine generator against this circ, if this circ is there
2278 $self->generate_fines_start if $self->circ;
2280 if( $self->checkin_check_holds_shelf() ) {
2281 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2282 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2283 $self->checkin_flesh_events;
2287 unless( $self->is_renewal ) {
2288 return $self->bail_on_events($self->editor->event)
2289 unless $self->editor->allowed('COPY_CHECKIN');
2292 $self->push_events($self->check_copy_alert());
2293 $self->push_events($self->check_checkin_copy_status());
2295 # if the circ is marked as 'claims returned', add the event to the list
2296 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2297 if ($self->circ and $self->circ->stop_fines
2298 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2300 $self->check_circ_deposit();
2302 # handle the overridable events
2303 $self->override_events unless $self->is_renewal;
2304 return if $self->bail_out;
2306 if( $self->copy and !$self->transit ) {
2308 $self->editor->search_action_transit_copy(
2309 { target_copy => $self->copy->id, dest_recv_time => undef }
2315 $self->generate_fines_finish;
2316 $self->checkin_handle_circ;
2317 return if $self->bail_out;
2318 $self->checkin_changed(1);
2320 } elsif( $self->transit ) {
2321 my $hold_transit = $self->process_received_transit;
2322 $self->checkin_changed(1);
2324 if( $self->bail_out ) {
2325 $self->checkin_flesh_events;
2329 if( my $e = $self->check_checkin_copy_status() ) {
2330 # If the original copy status is special, alert the caller
2331 my $ev = $self->events;
2332 $self->events([$e]);
2333 $self->override_events;
2334 return if $self->bail_out;
2338 if( $hold_transit or
2339 $U->copy_status($self->copy->status)->id
2340 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2343 if( $hold_transit ) {
2344 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2346 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2351 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2353 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2354 $self->reshelve_copy(1);
2355 $self->cancelled_hold_transit(1);
2356 $self->notify_hold(0); # don't notify for cancelled holds
2357 return if $self->bail_out;
2361 # hold transited to correct location
2362 $self->checkin_flesh_events;
2367 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2369 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2370 " that is in-transit, but there is no transit.. repairing");
2371 $self->reshelve_copy(1);
2372 return if $self->bail_out;
2375 if( $self->is_renewal ) {
2376 $self->finish_fines_and_voiding;
2377 return if $self->bail_out;
2378 $self->push_events(OpenILS::Event->new('SUCCESS'));
2382 # ------------------------------------------------------------------------------
2383 # Circulations and transits are now closed where necessary. Now go on to see if
2384 # this copy can fulfill a hold or needs to be routed to a different location
2385 # ------------------------------------------------------------------------------
2387 my $needed_for_something = 0; # formerly "needed_for_hold"
2389 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2391 if (!$self->remote_hold) {
2392 if ($self->use_booking) {
2393 my $potential_hold = $self->hold_capture_is_possible;
2394 my $potential_reservation = $self->reservation_capture_is_possible;
2396 if ($potential_hold and $potential_reservation) {
2397 $logger->info("circulator: item could fulfill either hold or reservation");
2398 $self->push_events(new OpenILS::Event(
2399 "HOLD_RESERVATION_CONFLICT",
2400 "hold" => $potential_hold,
2401 "reservation" => $potential_reservation
2403 return if $self->bail_out;
2404 } elsif ($potential_hold) {
2405 $needed_for_something =
2406 $self->attempt_checkin_hold_capture;
2407 } elsif ($potential_reservation) {
2408 $needed_for_something =
2409 $self->attempt_checkin_reservation_capture;
2412 $needed_for_something = $self->attempt_checkin_hold_capture;
2415 return if $self->bail_out;
2417 unless($needed_for_something) {
2418 my $circ_lib = (ref $self->copy->circ_lib) ?
2419 $self->copy->circ_lib->id : $self->copy->circ_lib;
2421 if( $self->remote_hold ) {
2422 $circ_lib = $self->remote_hold->pickup_lib;
2423 $logger->warn("circulator: Copy ".$self->copy->barcode.
2424 " is on a remote hold's shelf, sending to $circ_lib");
2427 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2429 if( $circ_lib == $self->circ_lib) {
2430 # copy is where it needs to be, either for hold or reshelving
2432 $self->checkin_handle_precat();
2433 return if $self->bail_out;
2436 # copy needs to transit "home", or stick here if it's a floating copy
2438 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2439 $self->checkin_changed(1);
2440 $self->copy->circ_lib( $self->circ_lib );
2443 my $bc = $self->copy->barcode;
2444 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2445 $self->checkin_build_copy_transit($circ_lib);
2446 return if $self->bail_out;
2447 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2451 } else { # no-op checkin
2452 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2453 $self->checkin_changed(1);
2454 $self->copy->circ_lib( $self->circ_lib );
2459 if($self->claims_never_checked_out and
2460 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2462 # the item was not supposed to be checked out to the user and should now be marked as missing
2463 $self->copy->status(OILS_COPY_STATUS_MISSING);
2467 $self->reshelve_copy unless $needed_for_something;
2470 return if $self->bail_out;
2472 unless($self->checkin_changed) {
2474 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2475 my $stat = $U->copy_status($self->copy->status)->id;
2477 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2478 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2479 $self->bail_out(1); # no need to commit anything
2483 $self->push_events(OpenILS::Event->new('SUCCESS'))
2484 unless @{$self->events};
2487 $self->finish_fines_and_voiding;
2489 OpenILS::Utils::Penalty->calculate_penalties(
2490 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2492 $self->checkin_flesh_events;
2496 sub finish_fines_and_voiding {
2498 return unless $self->circ;
2500 # gather any updates to the circ after fine generation, if there was a circ
2501 $self->generate_fines_finish;
2503 return unless $self->backdate or $self->void_overdues;
2505 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2506 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2508 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2509 $self->editor, $self->circ, $self->backdate, $note);
2511 return $self->bail_on_events($evt) if $evt;
2513 # make sure the circ isn't closed if we just voided some fines
2514 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2515 return $self->bail_on_events($evt) if $evt;
2521 # if a deposit was payed for this item, push the event
2522 sub check_circ_deposit {
2524 return unless $self->circ;
2525 my $deposit = $self->editor->search_money_billing(
2527 xact => $self->circ->id,
2529 }, {idlist => 1})->[0];
2531 $self->push_events(OpenILS::Event->new(
2532 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2537 my $force = $self->force || shift;
2538 my $copy = $self->copy;
2540 my $stat = $U->copy_status($copy->status)->id;
2543 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2544 $stat != OILS_COPY_STATUS_CATALOGING and
2545 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2546 $stat != OILS_COPY_STATUS_RESHELVING )) {
2548 $copy->status( OILS_COPY_STATUS_RESHELVING );
2550 $self->checkin_changed(1);
2555 # Returns true if the item is at the current location
2556 # because it was transited there for a hold and the
2557 # hold has not been fulfilled
2558 sub checkin_check_holds_shelf {
2560 return 0 unless $self->copy;
2563 $U->copy_status($self->copy->status)->id ==
2564 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2566 # find the hold that put us on the holds shelf
2567 my $holds = $self->editor->search_action_hold_request(
2569 current_copy => $self->copy->id,
2570 capture_time => { '!=' => undef },
2571 fulfillment_time => undef,
2572 cancel_time => undef,
2577 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2578 $self->reshelve_copy(1);
2582 my $hold = $$holds[0];
2584 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2585 $hold->id. "] for copy ".$self->copy->barcode);
2587 if( $hold->pickup_lib == $self->circ_lib ) {
2588 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2592 $logger->info("circulator: hold is not for here..");
2593 $self->remote_hold($hold);
2598 sub checkin_handle_precat {
2600 my $copy = $self->copy;
2602 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2603 $copy->status(OILS_COPY_STATUS_CATALOGING);
2604 $self->update_copy();
2605 $self->checkin_changed(1);
2606 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2611 sub checkin_build_copy_transit {
2614 my $copy = $self->copy;
2615 my $transit = Fieldmapper::action::transit_copy->new;
2617 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2618 $logger->info("circulator: transiting copy to $dest");
2620 $transit->source($self->circ_lib);
2621 $transit->dest($dest);
2622 $transit->target_copy($copy->id);
2623 $transit->source_send_time('now');
2624 $transit->copy_status( $U->copy_status($copy->status)->id );
2626 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2628 return $self->bail_on_events($self->editor->event)
2629 unless $self->editor->create_action_transit_copy($transit);
2631 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2633 $self->checkin_changed(1);
2637 sub hold_capture_is_possible {
2639 my $copy = $self->copy;
2641 # we've been explicitly told not to capture any holds
2642 return 0 if $self->capture eq 'nocapture';
2644 # See if this copy can fulfill any holds
2645 my $hold = $holdcode->find_nearest_permitted_hold(
2646 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2648 return undef if ref $hold eq "HASH" and
2649 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2653 sub reservation_capture_is_possible {
2655 my $copy = $self->copy;
2657 # we've been explicitly told not to capture any holds
2658 return 0 if $self->capture eq 'nocapture';
2660 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2661 my $resv = $booking_ses->request(
2662 "open-ils.booking.reservations.could_capture",
2663 $self->editor->authtoken, $copy->barcode
2665 $booking_ses->disconnect;
2666 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2667 $self->push_events($resv);
2673 # returns true if the item was used (or may potentially be used
2674 # in subsequent calls) to capture a hold.
2675 sub attempt_checkin_hold_capture {
2677 my $copy = $self->copy;
2679 # we've been explicitly told not to capture any holds
2680 return 0 if $self->capture eq 'nocapture';
2682 # See if this copy can fulfill any holds
2683 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2684 $self->editor, $copy, $self->editor->requestor );
2687 $logger->debug("circulator: no potential permitted".
2688 "holds found for copy ".$copy->barcode);
2692 if($self->capture ne 'capture') {
2693 # see if this item is in a hold-capture-delay location
2694 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2695 if($U->is_true($location->hold_verify)) {
2696 $self->bail_on_events(
2697 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2702 $self->retarget($retarget);
2704 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2706 $hold->current_copy($copy->id);
2707 $hold->capture_time('now');
2708 $self->put_hold_on_shelf($hold)
2709 if $hold->pickup_lib == $self->circ_lib;
2711 # prevent DB errors caused by fetching
2712 # holds from storage, and updating through cstore
2713 $hold->clear_fulfillment_time;
2714 $hold->clear_fulfillment_staff;
2715 $hold->clear_fulfillment_lib;
2716 $hold->clear_expire_time;
2717 $hold->clear_cancel_time;
2718 $hold->clear_prev_check_time unless $hold->prev_check_time;
2720 $self->bail_on_events($self->editor->event)
2721 unless $self->editor->update_action_hold_request($hold);
2723 $self->checkin_changed(1);
2725 return 0 if $self->bail_out;
2727 if( $hold->pickup_lib == $self->circ_lib ) {
2729 # This hold was captured in the correct location
2730 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2731 $self->push_events(OpenILS::Event->new('SUCCESS'));
2733 #$self->do_hold_notify($hold->id);
2734 $self->notify_hold($hold->id);
2738 # Hold needs to be picked up elsewhere. Build a hold
2739 # transit and route the item.
2740 $self->checkin_build_hold_transit();
2741 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2742 return 0 if $self->bail_out;
2743 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2746 # make sure we save the copy status
2751 sub attempt_checkin_reservation_capture {
2753 my $copy = $self->copy;
2755 # we've been explicitly told not to capture any holds
2756 return 0 if $self->capture eq 'nocapture';
2758 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2759 my $evt = $booking_ses->request(
2760 "open-ils.booking.resources.capture_for_reservation",
2761 $self->editor->authtoken,
2763 1 # don't update copy - we probably have it locked
2765 $booking_ses->disconnect;
2767 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2769 "open-ils.booking.resources.capture_for_reservation " .
2770 "didn't return an event!"
2774 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2775 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2777 # not-transferable is an error event we'll pass on the user
2778 $logger->warn("reservation capture attempted against non-transferable item");
2779 $self->push_events($evt);
2781 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2782 # Re-retrieve copy as reservation capture may have changed
2783 # its status and whatnot.
2785 "circulator: booking capture win on copy " . $self->copy->id
2787 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2789 "circulator: changing copy " . $self->copy->id .
2790 "'s status from " . $self->copy->status . " to " .
2793 $self->copy->status($new_copy_status);
2796 $self->reservation($evt->{"payload"}->{"reservation"});
2798 if (exists $evt->{"payload"}->{"transit"}) {
2802 "org" => $evt->{"payload"}->{"transit"}->dest
2806 $self->checkin_changed(1);
2810 # other results are treated as "nothing to capture"
2814 sub do_hold_notify {
2815 my( $self, $holdid ) = @_;
2817 my $e = new_editor(xact => 1);
2818 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2820 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2821 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2823 $logger->info("circulator: running delayed hold notify process");
2825 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2826 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2828 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2829 hold_id => $holdid, requestor => $self->editor->requestor);
2831 $logger->debug("circulator: built hold notifier");
2833 if(!$notifier->event) {
2835 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2837 my $stat = $notifier->send_email_notify;
2838 if( $stat == '1' ) {
2839 $logger->info("circulator: hold notify succeeded for hold $holdid");
2843 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2846 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2850 sub retarget_holds {
2852 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2853 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2854 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2855 # no reason to wait for the return value
2859 sub checkin_build_hold_transit {
2862 my $copy = $self->copy;
2863 my $hold = $self->hold;
2864 my $trans = Fieldmapper::action::hold_transit_copy->new;
2866 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2868 $trans->hold($hold->id);
2869 $trans->source($self->circ_lib);
2870 $trans->dest($hold->pickup_lib);
2871 $trans->source_send_time("now");
2872 $trans->target_copy($copy->id);
2874 # when the copy gets to its destination, it will recover
2875 # this status - put it onto the holds shelf
2876 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2878 return $self->bail_on_events($self->editor->event)
2879 unless $self->editor->create_action_hold_transit_copy($trans);
2884 sub process_received_transit {
2886 my $copy = $self->copy;
2887 my $copyid = $self->copy->id;
2889 my $status_name = $U->copy_status($copy->status)->name;
2890 $logger->debug("circulator: attempting transit receive on ".
2891 "copy $copyid. Copy status is $status_name");
2893 my $transit = $self->transit;
2895 if( $transit->dest != $self->circ_lib ) {
2896 # - this item is in-transit to a different location
2898 my $tid = $transit->id;
2899 my $loc = $self->circ_lib;
2900 my $dest = $transit->dest;
2902 $logger->info("circulator: Fowarding transit on copy which is destined ".
2903 "for a different location. transit=$tid, copy=$copyid, current ".
2904 "location=$loc, destination location=$dest");
2906 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2908 # grab the associated hold object if available
2909 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2910 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2912 return $self->bail_on_events($evt);
2915 # The transit is received, set the receive time
2916 $transit->dest_recv_time('now');
2917 $self->bail_on_events($self->editor->event)
2918 unless $self->editor->update_action_transit_copy($transit);
2920 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2922 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2923 $copy->status( $transit->copy_status );
2924 $self->update_copy();
2925 return if $self->bail_out;
2929 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2931 # hold has arrived at destination, set shelf time
2932 $self->put_hold_on_shelf($hold);
2933 $self->bail_on_events($self->editor->event)
2934 unless $self->editor->update_action_hold_request($hold);
2935 return if $self->bail_out;
2937 $self->notify_hold($hold_transit->hold);
2942 OpenILS::Event->new(
2945 payload => { transit => $transit, holdtransit => $hold_transit } ));
2947 return $hold_transit;
2951 # ------------------------------------------------------------------
2952 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2953 # ------------------------------------------------------------------
2954 sub put_hold_on_shelf {
2955 my($self, $hold) = @_;
2957 $hold->shelf_time('now');
2959 my $shelf_expire = $U->ou_ancestor_setting_value(
2960 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2962 return undef unless $shelf_expire;
2964 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2965 my $expire_time = DateTime->now->add(seconds => $seconds);
2967 # if the shelf expire time overlaps with a pickup lib's
2968 # closed date, push it out to the first open date
2969 my $dateinfo = $U->storagereq(
2970 'open-ils.storage.actor.org_unit.closed_date.overlap',
2971 $hold->pickup_lib, $expire_time);
2974 my $dt_parser = DateTime::Format::ISO8601->new;
2975 $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
2977 # TODO: enable/disable time bump via setting?
2978 $expire_time->set(hour => '23', minute => '59', second => '59');
2980 $logger->info("circulator: shelf_expire_time overlaps".
2981 " with closed date, pushing expire time to $expire_time");
2984 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2990 sub generate_fines {
2992 my $reservation = shift;
2994 $self->generate_fines_start($reservation);
2995 $self->generate_fines_finish($reservation);
3000 sub generate_fines_start {
3002 my $reservation = shift;
3003 my $dt_parser = DateTime::Format::ISO8601->new;
3005 my $obj = $reservation ? $self->reservation : $self->circ;
3007 # If we have a grace period
3008 if($obj->can('grace_period')) {
3009 # Parse out the due date
3010 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3011 # Add the grace period to the due date
3012 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3013 # Don't generate fines on circs still in grace period
3014 return undef if ($due_date > DateTime->now);
3017 if (!exists($self->{_gen_fines_req})) {
3018 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3020 'open-ils.storage.action.circulation.overdue.generate_fines',
3028 sub generate_fines_finish {
3030 my $reservation = shift;
3032 return undef unless $self->{_gen_fines_req};
3034 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3036 $self->{_gen_fines_req}->wait_complete;
3037 delete($self->{_gen_fines_req});
3039 # refresh the circ in case the fine generator set the stop_fines field
3040 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3041 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3046 sub checkin_handle_circ {
3048 my $circ = $self->circ;
3049 my $copy = $self->copy;
3053 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3055 # backdate the circ if necessary
3056 if($self->backdate) {
3057 my $evt = $self->checkin_handle_backdate;
3058 return $self->bail_on_events($evt) if $evt;
3061 if(!$circ->stop_fines) {
3062 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3063 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3064 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3065 $circ->stop_fines_time('now');
3066 $circ->stop_fines_time($self->backdate) if $self->backdate;
3069 # Set the checkin vars since we have the item
3070 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3072 # capture the true scan time for back-dated checkins
3073 $circ->checkin_scan_time('now');
3075 $circ->checkin_staff($self->editor->requestor->id);
3076 $circ->checkin_lib($self->circ_lib);
3077 $circ->checkin_workstation($self->editor->requestor->wsid);
3079 my $circ_lib = (ref $self->copy->circ_lib) ?
3080 $self->copy->circ_lib->id : $self->copy->circ_lib;
3081 my $stat = $U->copy_status($self->copy->status)->id;
3083 # immediately available keeps items lost or missing items from going home before being handled
3084 my $lost_immediately_available = $U->ou_ancestor_setting_value(
3085 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3088 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
3090 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
3091 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
3093 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3097 } elsif ($stat == OILS_COPY_STATUS_LOST) {
3099 $self->checkin_handle_lost($circ_lib);
3103 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3108 # see if there are any fines owed on this circ. if not, close it
3109 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3110 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3112 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3114 return $self->bail_on_events($self->editor->event)
3115 unless $self->editor->update_action_circulation($circ);
3121 # ------------------------------------------------------------------
3122 # See if we need to void billings for lost checkin
3123 # ------------------------------------------------------------------
3124 sub checkin_handle_lost {
3126 my $circ_lib = shift;
3127 my $circ = $self->circ;
3129 my $max_return = $U->ou_ancestor_setting_value(
3130 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3135 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3136 $tm[5] -= 1 if $tm[5] > 0;
3137 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3139 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3140 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3142 $max_return = 0 if $today < $last_chance;
3145 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3147 my $void_lost = $U->ou_ancestor_setting_value(
3148 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3149 my $void_lost_fee = $U->ou_ancestor_setting_value(
3150 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3151 my $restore_od = $U->ou_ancestor_setting_value(
3152 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3153 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3154 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3156 $self->checkin_handle_lost_now_found(3) if $void_lost;
3157 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3158 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3161 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3166 sub checkin_handle_backdate {
3169 # ------------------------------------------------------------------
3170 # clean up the backdate for date comparison
3171 # XXX We are currently taking the due-time from the original due-date,
3172 # not the input. Do we need to do this? This certainly interferes with
3173 # backdating of hourly checkouts, but that is likely a very rare case.
3174 # ------------------------------------------------------------------
3175 my $bd = cleanse_ISO8601($self->backdate);
3176 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3177 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3178 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3180 $self->backdate($bd);
3185 sub check_checkin_copy_status {
3187 my $copy = $self->copy;
3189 my $status = $U->copy_status($copy->status)->id;
3192 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3193 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3194 $status == OILS_COPY_STATUS_IN_PROCESS ||
3195 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3196 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3197 $status == OILS_COPY_STATUS_CATALOGING ||
3198 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3199 $status == OILS_COPY_STATUS_RESHELVING );
3201 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3202 if( $status == OILS_COPY_STATUS_LOST );
3204 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3205 if( $status == OILS_COPY_STATUS_MISSING );
3207 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3212 # --------------------------------------------------------------------------
3213 # On checkin, we need to return as many relevant objects as we can
3214 # --------------------------------------------------------------------------
3215 sub checkin_flesh_events {
3218 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3219 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3220 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3223 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3226 if($self->hold and !$self->hold->cancel_time) {
3227 $hold = $self->hold;
3228 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3232 # if we checked in a circulation, flesh the billing summary data
3233 $self->circ->billable_transaction(
3234 $self->editor->retrieve_money_billable_transaction([
3236 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3242 # flesh some patron fields before returning
3244 $self->editor->retrieve_actor_user([
3249 au => ['card', 'billing_address', 'mailing_address']
3256 for my $evt (@{$self->events}) {
3259 $payload->{copy} = $U->unflesh_copy($self->copy);
3260 $payload->{volume} = $self->volume;
3261 $payload->{record} = $record,
3262 $payload->{circ} = $self->circ;
3263 $payload->{transit} = $self->transit;
3264 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3265 $payload->{hold} = $hold;
3266 $payload->{patron} = $self->patron;
3267 $payload->{reservation} = $self->reservation
3268 unless (not $self->reservation or $self->reservation->cancel_time);
3270 $evt->{payload} = $payload;
3275 my( $self, $msg ) = @_;
3276 my $bc = ($self->copy) ? $self->copy->barcode :
3279 my $usr = ($self->patron) ? $self->patron->id : "";
3280 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3281 ", recipient=$usr, copy=$bc");
3287 $self->log_me("do_renew()");
3289 # Make sure there is an open circ to renew that is not
3290 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3291 my $usrid = $self->patron->id if $self->patron;
3292 my $circ = $self->editor->search_action_circulation({
3293 target_copy => $self->copy->id,
3294 xact_finish => undef,
3295 checkin_time => undef,
3296 ($usrid ? (usr => $usrid) : ()),
3298 {stop_fines => undef},
3299 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3303 return $self->bail_on_events($self->editor->event) unless $circ;
3305 # A user is not allowed to renew another user's items without permission
3306 unless( $circ->usr eq $self->editor->requestor->id ) {
3307 return $self->bail_on_events($self->editor->events)
3308 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3311 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3312 if $circ->renewal_remaining < 1;
3314 # -----------------------------------------------------------------
3316 $self->parent_circ($circ->id);
3317 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3320 # Opac renewal - re-use circ library from original circ (unless told not to)
3321 if($self->opac_renewal) {
3322 unless(defined($opac_renewal_use_circ_lib)) {
3323 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3324 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3325 $opac_renewal_use_circ_lib = 1;
3328 $opac_renewal_use_circ_lib = 0;
3331 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3334 # Run the fine generator against the old circ
3335 $self->generate_fines_start;
3337 $self->run_renew_permit;
3340 $self->do_checkin();
3341 return if $self->bail_out;
3343 unless( $self->permit_override ) {
3345 return if $self->bail_out;
3346 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3347 $self->remove_event('ITEM_NOT_CATALOGED');
3350 $self->override_events;
3351 return if $self->bail_out;
3354 $self->do_checkout();
3359 my( $self, $evt ) = @_;
3360 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3361 $logger->debug("circulator: removing event from list: $evt");
3362 my @events = @{$self->events};
3363 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3368 my( $self, $evt ) = @_;
3369 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3370 return grep { $_->{textcode} eq $evt } @{$self->events};
3375 sub run_renew_permit {
3378 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3379 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3380 $self->editor, $self->copy, $self->editor->requestor, 1
3382 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3385 if(!$self->legacy_script_support) {
3386 my $results = $self->run_indb_circ_test;
3387 $self->push_events($self->matrix_test_result_events)
3388 unless $self->circ_test_success;
3391 my $runner = $self->script_runner;
3393 $runner->load($self->circ_permit_renew);
3394 my $result = $runner->run or
3395 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3396 if ($result->{"events"}) {
3398 map { new OpenILS::Event($_) } @{$result->{"events"}}
3401 "circulator: circ_permit_renew for user " .
3402 $self->patron->id . " returned " .
3403 scalar(@{$result->{"events"}}) . " event(s)"
3407 $self->mk_script_runner;
3410 $logger->debug("circulator: re-creating script runner to be safe");
3414 # XXX: The primary mechanism for storing circ history is now handled
3415 # by tracking real circulation objects instead of bibs in a bucket.
3416 # However, this code is disabled by default and could be useful
3417 # some day, so may as well leave it for now.
3418 sub append_reading_list {
3422 $self->is_checkout and
3428 # verify history is globally enabled and uses the bucket mechanism
3429 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3430 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3432 return undef unless $htype and $htype eq 'bucket';
3434 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3436 # verify the patron wants to retain the hisory
3437 my $setting = $e->search_actor_user_setting(
3438 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3440 unless($setting and $setting->value) {
3445 my $bkt = $e->search_container_copy_bucket(
3446 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3451 # find the next item position
3452 my $last_item = $e->search_container_copy_bucket_item(
3453 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3454 $pos = $last_item->pos + 1 if $last_item;
3457 # create the history bucket if necessary
3458 $bkt = Fieldmapper::container::copy_bucket->new;
3459 $bkt->owner($self->patron->id);
3461 $bkt->btype('circ_history');
3463 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3466 my $item = Fieldmapper::container::copy_bucket_item->new;
3468 $item->bucket($bkt->id);
3469 $item->target_copy($self->copy->id);
3472 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3479 sub make_trigger_events {
3481 return unless $self->circ;
3482 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3483 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3484 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3489 sub checkin_handle_lost_now_found {
3490 my ($self, $bill_type) = @_;
3492 # ------------------------------------------------------------------
3493 # remove charge from patron's account if lost item is returned
3494 # ------------------------------------------------------------------
3496 my $bills = $self->editor->search_money_billing(
3498 xact => $self->circ->id,
3503 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3504 for my $bill (@$bills) {
3505 if( !$U->is_true($bill->voided) ) {
3506 $logger->info("lost item returned - voiding bill ".$bill->id);
3508 $bill->void_time('now');
3509 $bill->voider($self->editor->requestor->id);
3510 my $note = ($bill->note) ? $bill->note . "\n" : '';
3511 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3513 $self->bail_on_events($self->editor->event)
3514 unless $self->editor->update_money_billing($bill);
3519 sub checkin_handle_lost_now_found_restore_od {
3521 my $circ_lib = shift;
3523 # ------------------------------------------------------------------
3524 # restore those overdue charges voided when item was set to lost
3525 # ------------------------------------------------------------------
3527 my $ods = $self->editor->search_money_billing(
3529 xact => $self->circ->id,
3534 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3535 for my $bill (@$ods) {
3536 if( $U->is_true($bill->voided) ) {
3537 $logger->info("lost item returned - restoring overdue ".$bill->id);
3539 $bill->clear_void_time;
3540 $bill->voider($self->editor->requestor->id);
3541 my $note = ($bill->note) ? $bill->note . "\n" : '';
3542 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3544 $self->bail_on_events($self->editor->event)
3545 unless $self->editor->update_money_billing($bill);
3550 # ------------------------------------------------------------------
3551 # Lost-then-found item checked in. This sub generates new overdue
3552 # fines, beyond the point of any existing and possibly voided
3553 # overdue fines, up to the point of final checkin time (or max fine
3555 # ------------------------------------------------------------------
3556 sub generate_lost_overdue_fines {
3558 my $circ = $self->circ;
3559 my $e = $self->editor;
3561 # Re-open the transaction so the fine generator can see it
3562 if($circ->xact_finish or $circ->stop_fines) {
3564 $circ->clear_xact_finish;
3565 $circ->clear_stop_fines;
3566 $circ->clear_stop_fines_time;
3567 $e->update_action_circulation($circ) or return $e->die_event;
3571 $e->xact_begin; # generate_fines expects an in-xact editor
3572 $self->generate_fines;
3573 $circ = $self->circ; # generate fines re-fetches the circ
3577 # Re-close the transaction if no money is owed
3578 my ($obt) = $U->fetch_mbts($circ->id, $e);
3579 if ($obt and $obt->balance_owed == 0) {
3580 $circ->xact_finish('now');
3584 # Set stop fines if the fine generator didn't have to
3585 unless($circ->stop_fines) {
3586 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3587 $circ->stop_fines_time('now');
3591 # update the event data sent to the caller within the transaction
3592 $self->checkin_flesh_events;
3595 $e->update_action_circulation($circ) or return $e->die_event;