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;
18 my $desk_renewal_use_circ_lib;
20 sub determine_booking_status {
21 unless (defined $booking_status) {
22 my $ses = create OpenSRF::AppSession("router");
23 $booking_status = grep {$_ eq "open-ils.booking"} @{
24 $ses->request("opensrf.router.info.class.list")->gather(1)
27 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
30 return $booking_status;
36 flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
42 my $conf = OpenSRF::Utils::SettingsClient->new;
43 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
45 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
46 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
48 my $lb = $conf->config_value( @pfx2, 'script_path' );
49 $lb = [ $lb ] unless ref($lb);
52 return unless $legacy_script_support;
54 my @pfx = ( @pfx2, "scripts" );
55 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
56 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
57 my $d = $conf->config_value( @pfx, 'circ_duration' );
58 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
59 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
60 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
62 $logger->error( "Missing circ script(s)" )
63 unless( $p and $c and $d and $f and $m and $pr );
65 $scripts{circ_permit_patron} = $p;
66 $scripts{circ_permit_copy} = $c;
67 $scripts{circ_duration} = $d;
68 $scripts{circ_recurring_fines} = $f;
69 $scripts{circ_max_fines} = $m;
70 $scripts{circ_permit_renew} = $pr;
73 "circulator: Loaded rules scripts for circ: " .
74 "circ permit patron = $p, ".
75 "circ permit copy = $c, ".
76 "circ duration = $d, ".
77 "circ recurring fines = $f, " .
78 "circ max fines = $m, ".
79 "circ renew permit = $pr. ".
81 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
85 __PACKAGE__->register_method(
86 method => "run_method",
87 api_name => "open-ils.circ.checkout.permit",
89 Determines if the given checkout can occur
90 @param authtoken The login session key
91 @param params A trailing hash of named params including
92 barcode : The copy barcode,
93 patron : The patron the checkout is occurring for,
94 renew : true or false - whether or not this is a renewal
95 @return The event that occurred during the permit check.
99 __PACKAGE__->register_method (
100 method => 'run_method',
101 api_name => 'open-ils.circ.checkout.permit.override',
102 signature => q/@see open-ils.circ.checkout.permit/,
106 __PACKAGE__->register_method(
107 method => "run_method",
108 api_name => "open-ils.circ.checkout",
111 @param authtoken The login session key
112 @param params A named hash of params including:
114 barcode If no copy is provided, the copy is retrieved via barcode
115 copyid If no copy or barcode is provide, the copy id will be use
116 patron The patron's id
117 noncat True if this is a circulation for a non-cataloted item
118 noncat_type The non-cataloged type id
119 noncat_circ_lib The location for the noncat circ.
120 precat The item has yet to be cataloged
121 dummy_title The temporary title of the pre-cataloded item
122 dummy_author The temporary authr of the pre-cataloded item
123 Default is the home org of the staff member
124 @return The SUCCESS event on success, any other event depending on the error
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.checkin",
132 Generic super-method for handling all copies
133 @param authtoken The login session key
134 @param params Hash of named parameters including:
135 barcode - The copy barcode
136 force - If true, copies in bad statuses will be checked in and give good statuses
137 noop - don't capture holds or put items into transit
138 void_overdues - void all overdues for the circulation (aka amnesty)
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkin.override",
146 signature => q/@see open-ils.circ.checkin/
149 __PACKAGE__->register_method(
150 method => "run_method",
151 api_name => "open-ils.circ.renew.override",
152 signature => q/@see open-ils.circ.renew/,
156 __PACKAGE__->register_method(
157 method => "run_method",
158 api_name => "open-ils.circ.renew",
159 notes => <<" NOTES");
160 PARAMS( authtoken, circ => circ_id );
161 open-ils.circ.renew(login_session, circ_object);
162 Renews the provided circulation. login_session is the requestor of the
163 renewal and if the logged in user is not the same as circ->usr, then
164 the logged in user must have RENEW_CIRC permissions.
167 __PACKAGE__->register_method(
168 method => "run_method",
169 api_name => "open-ils.circ.checkout.full"
171 __PACKAGE__->register_method(
172 method => "run_method",
173 api_name => "open-ils.circ.checkout.full.override"
175 __PACKAGE__->register_method(
176 method => "run_method",
177 api_name => "open-ils.circ.reservation.pickup"
179 __PACKAGE__->register_method(
180 method => "run_method",
181 api_name => "open-ils.circ.reservation.return"
183 __PACKAGE__->register_method(
184 method => "run_method",
185 api_name => "open-ils.circ.reservation.return.override"
187 __PACKAGE__->register_method(
188 method => "run_method",
189 api_name => "open-ils.circ.checkout.inspect",
190 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
195 my( $self, $conn, $auth, $args ) = @_;
196 translate_legacy_args($args);
197 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
198 my $api = $self->api_name;
201 OpenILS::Application::Circ::Circulator->new($auth, %$args);
203 return circ_events($circulator) if $circulator->bail_out;
205 $circulator->use_booking(determine_booking_status());
207 # --------------------------------------------------------------------------
208 # First, check for a booking transit, as the barcode may not be a copy
209 # barcode, but a resource barcode, and nothing else in here will work
210 # --------------------------------------------------------------------------
212 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
213 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
214 if (@$resources) { # yes!
216 my $res_id_list = [ map { $_->id } @$resources ];
217 my $transit = $circulator->editor->search_action_reservation_transit_copy(
219 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
220 { order_by => { artc => 'source_send_time' }, limit => 1 }
222 )->[0]; # Any transit for this barcode?
224 if ($transit) { # yes! unwrap it.
226 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
227 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
229 my $success_event = new OpenILS::Event(
230 "SUCCESS", "payload" => {"reservation" => $reservation}
232 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
233 if (my $copy = $circulator->editor->search_asset_copy([
234 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
235 ])->[0]) { # got a copy
236 $copy->status( $transit->copy_status );
237 $copy->editor($circulator->editor->requestor->id);
238 $copy->edit_date('now');
239 $circulator->editor->update_asset_copy($copy);
240 $success_event->{"payload"}->{"record"} =
241 $U->record_to_mvr($copy->call_number->record);
242 $success_event->{"payload"}->{"volume"} = $copy->call_number;
243 $copy->call_number($copy->call_number->id);
244 $success_event->{"payload"}->{"copy"} = $copy;
248 $transit->dest_recv_time('now');
249 $circulator->editor->update_action_reservation_transit_copy( $transit );
251 $circulator->editor->commit;
252 # Formerly this branch just stopped here. Argh!
253 $conn->respond_complete($success_event);
261 # --------------------------------------------------------------------------
262 # Go ahead and load the script runner to make sure we have all
263 # of the objects we need
264 # --------------------------------------------------------------------------
266 if ($circulator->use_booking) {
267 $circulator->is_res_checkin($circulator->is_checkin(1))
268 if $api =~ /reservation.return/ or (
269 $api =~ /checkin/ and $circulator->seems_like_reservation()
272 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
275 $circulator->is_renewal(1) if $api =~ /renew/;
276 $circulator->is_checkin(1) if $api =~ /checkin/;
278 $circulator->mk_env();
279 $circulator->noop(1) if $circulator->claims_never_checked_out;
281 if($legacy_script_support and not $circulator->is_checkin) {
282 $circulator->mk_script_runner();
283 $circulator->legacy_script_support(1);
284 $circulator->circ_permit_patron($scripts{circ_permit_patron});
285 $circulator->circ_permit_copy($scripts{circ_permit_copy});
286 $circulator->circ_duration($scripts{circ_duration});
287 $circulator->circ_permit_renew($scripts{circ_permit_renew});
289 return circ_events($circulator) if $circulator->bail_out;
292 $circulator->override(1) if $api =~ /override/o;
294 if( $api =~ /checkout\.permit/ ) {
295 $circulator->do_permit();
297 } elsif( $api =~ /checkout.full/ ) {
299 # requesting a precat checkout implies that any required
300 # overrides have been performed. Go ahead and re-override.
301 $circulator->skip_permit_key(1);
302 $circulator->override(1) if $circulator->request_precat;
303 $circulator->do_permit();
304 $circulator->is_checkout(1);
305 unless( $circulator->bail_out ) {
306 $circulator->events([]);
307 $circulator->do_checkout();
310 } elsif( $circulator->is_res_checkout ) {
311 $circulator->do_reservation_pickup();
313 } elsif( $api =~ /inspect/ ) {
314 my $data = $circulator->do_inspect();
315 $circulator->editor->rollback;
318 } elsif( $api =~ /checkout/ ) {
319 $circulator->is_checkout(1);
320 $circulator->do_checkout();
322 } elsif( $circulator->is_res_checkin ) {
323 $circulator->do_reservation_return();
324 $circulator->do_checkin() if ($circulator->copy());
325 } elsif( $api =~ /checkin/ ) {
326 $circulator->do_checkin();
328 } elsif( $api =~ /renew/ ) {
329 $circulator->is_renewal(1);
330 $circulator->do_renew();
333 if( $circulator->bail_out ) {
336 # make sure no success event accidentally slip in
338 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
341 my @e = @{$circulator->events};
342 push( @ee, $_->{textcode} ) for @e;
343 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
345 $circulator->editor->rollback;
349 $circulator->editor->commit;
351 if ($circulator->generate_lost_overdue) {
352 # Generating additional overdue billings has to happen after the
353 # main commit and before the final respond() so the caller can
354 # receive the latest transaction summary.
355 my $evt = $circulator->generate_lost_overdue_fines;
356 $circulator->bail_on_events($evt) if $evt;
360 $conn->respond_complete(circ_events($circulator));
362 $circulator->script_runner->cleanup if $circulator->script_runner;
364 return undef if $circulator->bail_out;
366 $circulator->do_hold_notify($circulator->notify_hold)
367 if $circulator->notify_hold;
368 $circulator->retarget_holds if $circulator->retarget;
369 $circulator->append_reading_list;
370 $circulator->make_trigger_events;
377 my @e = @{$circ->events};
378 # if we have multiple events, SUCCESS should not be one of them;
379 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
380 return (@e == 1) ? $e[0] : \@e;
384 sub translate_legacy_args {
387 if( $$args{barcode} ) {
388 $$args{copy_barcode} = $$args{barcode};
389 delete $$args{barcode};
392 if( $$args{copyid} ) {
393 $$args{copy_id} = $$args{copyid};
394 delete $$args{copyid};
397 if( $$args{patronid} ) {
398 $$args{patron_id} = $$args{patronid};
399 delete $$args{patronid};
402 if( $$args{patron} and !ref($$args{patron}) ) {
403 $$args{patron_id} = $$args{patron};
404 delete $$args{patron};
408 if( $$args{noncat} ) {
409 $$args{is_noncat} = $$args{noncat};
410 delete $$args{noncat};
413 if( $$args{precat} ) {
414 $$args{is_precat} = $$args{request_precat} = $$args{precat};
415 delete $$args{precat};
421 # --------------------------------------------------------------------------
422 # This package actually manages all of the circulation logic
423 # --------------------------------------------------------------------------
424 package OpenILS::Application::Circ::Circulator;
425 use strict; use warnings;
426 use vars q/$AUTOLOAD/;
428 use OpenILS::Utils::Fieldmapper;
429 use OpenSRF::Utils::Cache;
430 use Digest::MD5 qw(md5_hex);
431 use DateTime::Format::ISO8601;
432 use OpenILS::Utils::PermitHold;
433 use OpenSRF::Utils qw/:datetime/;
434 use OpenSRF::Utils::SettingsClient;
435 use OpenILS::Application::Circ::Holds;
436 use OpenILS::Application::Circ::Transit;
437 use OpenSRF::Utils::Logger qw(:logger);
438 use OpenILS::Utils::CStoreEditor qw/:funcs/;
439 use OpenILS::Application::Circ::ScriptBuilder;
440 use OpenILS::Const qw/:const/;
441 use OpenILS::Utils::Penalty;
442 use OpenILS::Application::Circ::CircCommon;
445 my $holdcode = "OpenILS::Application::Circ::Holds";
446 my $transcode = "OpenILS::Application::Circ::Transit";
452 # --------------------------------------------------------------------------
453 # Add a pile of automagic getter/setter methods
454 # --------------------------------------------------------------------------
455 my @AUTOLOAD_FIELDS = qw/
502 recurring_fines_level
515 cancelled_hold_transit
522 circ_matrix_matchpoint
524 legacy_script_support
534 claims_never_checked_out
539 generate_lost_overdue
553 my $type = ref($self) or die "$self is not an object";
555 my $name = $AUTOLOAD;
558 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
559 $logger->error("circulator: $type: invalid autoload field: $name");
560 die "$type: invalid autoload field: $name\n"
565 *{"${type}::${name}"} = sub {
568 $s->{$name} = $v if defined $v;
572 return $self->$name($data);
577 my( $class, $auth, %args ) = @_;
578 $class = ref($class) || $class;
579 my $self = bless( {}, $class );
582 $self->editor(new_editor(xact => 1, authtoken => $auth));
584 unless( $self->editor->checkauth ) {
585 $self->bail_on_events($self->editor->event);
589 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
591 $self->$_($args{$_}) for keys %args;
594 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
596 # if this is a renewal, default to desk_renewal
597 $self->desk_renewal(1) unless
598 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
600 $self->capture('') unless $self->capture;
602 unless(%user_groups) {
603 my $gps = $self->editor->retrieve_all_permission_grp_tree;
604 %user_groups = map { $_->id => $_ } @$gps;
611 # --------------------------------------------------------------------------
612 # True if we should discontinue processing
613 # --------------------------------------------------------------------------
615 my( $self, $bool ) = @_;
616 if( defined $bool ) {
617 $logger->info("circulator: BAILING OUT") if $bool;
618 $self->{bail_out} = $bool;
620 return $self->{bail_out};
625 my( $self, @evts ) = @_;
628 $e->{payload} = $self->copy if
629 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
631 $logger->info("circulator: pushing event ".$e->{textcode});
632 push( @{$self->events}, $e ) unless
633 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
639 return '' if $self->skip_permit_key;
640 my $key = md5_hex( time() . rand() . "$$" );
641 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
642 return $self->permit_key($key);
645 sub check_permit_key {
647 return 1 if $self->skip_permit_key;
648 my $key = $self->permit_key;
649 return 0 unless $key;
650 my $k = "oils_permit_key_$key";
651 my $one = $self->cache_handle->get_cache($k);
652 $self->cache_handle->delete_cache($k);
653 return ($one) ? 1 : 0;
656 sub seems_like_reservation {
659 # Some words about the following method:
660 # 1) It requires the VIEW_USER permission, but that's not an
661 # issue, right, since all staff should have that?
662 # 2) It returns only one reservation at a time, even if an item can be
663 # and is currently overbooked. Hmmm....
664 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
665 my $result = $booking_ses->request(
666 "open-ils.booking.reservations.by_returnable_resource_barcode",
667 $self->editor->authtoken,
670 $booking_ses->disconnect;
672 return $self->bail_on_events($result) if defined $U->event_code($result);
675 $self->reservation(shift @$result);
683 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
684 sub save_trimmed_copy {
685 my ($self, $copy) = @_;
688 $self->volume($copy->call_number);
689 $self->title($self->volume->record);
690 $self->copy->call_number($self->volume->id);
691 $self->volume->record($self->title->id);
692 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
693 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
694 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
695 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
701 my $e = $self->editor;
703 # --------------------------------------------------------------------------
704 # Grab the fleshed copy
705 # --------------------------------------------------------------------------
706 unless($self->is_noncat) {
709 $copy = $e->retrieve_asset_copy(
710 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
712 } elsif( $self->copy_barcode ) {
714 $copy = $e->search_asset_copy(
715 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
716 } elsif( $self->reservation ) {
717 my $res = $e->json_query(
719 "select" => {"acp" => ["id"]},
724 "field" => "barcode",
728 "field" => "current_resource"
736 "id" => (ref $self->reservation) ?
737 $self->reservation->id : $self->reservation
742 if (ref $res eq "ARRAY" and scalar @$res) {
743 $logger->info("circulator: mapped reservation " .
744 $self->reservation . " to copy " . $res->[0]->{"id"});
745 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
750 $self->save_trimmed_copy($copy);
752 # We can't renew if there is no copy
753 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
754 if $self->is_renewal;
759 # --------------------------------------------------------------------------
761 # --------------------------------------------------------------------------
765 flesh_fields => {au => [ qw/ card / ]}
768 if( $self->patron_id ) {
769 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
770 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
772 } elsif( $self->patron_barcode ) {
774 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
775 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
776 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
778 $patron = $e->retrieve_actor_user($card->usr)
779 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
781 # Use the card we looked up, not the patron's primary, for card active checks
782 $patron->card($card);
785 if( my $copy = $self->copy ) {
788 $flesh->{flesh_fields}->{circ} = ['usr'];
790 my $circ = $e->search_action_circulation([
791 {target_copy => $copy->id, checkin_time => undef}, $flesh
795 $patron = $circ->usr;
796 $circ->usr($patron->id); # de-flesh for consistency
802 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
803 unless $self->patron($patron) or $self->is_checkin;
805 unless($self->is_checkin) {
807 # Check for inactivity and patron reg. expiration
809 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
810 unless $U->is_true($patron->active);
812 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
813 unless $U->is_true($patron->card->active);
815 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
816 cleanse_ISO8601($patron->expire_date));
818 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
819 if( CORE::time > $expire->epoch ) ;
823 # --------------------------------------------------------------------------
824 # This builds the script runner environment and fetches most of the
826 # --------------------------------------------------------------------------
827 sub mk_script_runner {
833 qw/copy copy_barcode copy_id patron
834 patron_id patron_barcode volume title editor/;
836 # Translate our objects into the ScriptBuilder args hash
837 $$args{$_} = $self->$_() for @fields;
839 $args->{ignore_user_status} = 1 if $self->is_checkin;
840 $$args{fetch_patron_by_circ_copy} = 1;
841 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
843 if( my $pco = $self->pending_checkouts ) {
844 $logger->info("circulator: we were given a pending checkouts number of $pco");
845 $$args{patronItemsOut} = $pco;
848 # This fetches most of the objects we need
849 $self->script_runner(
850 OpenILS::Application::Circ::ScriptBuilder->build($args));
852 # Now we translate the ScriptBuilder objects back into self
853 $self->$_($$args{$_}) for @fields;
855 my @evts = @{$args->{_events}} if $args->{_events};
857 $logger->debug("circulator: script builder returned events: @evts") if @evts;
861 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
862 if(!$self->is_noncat and
864 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
868 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
869 return $self->bail_on_events(@e);
874 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
875 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
876 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
877 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
881 # We can't renew if there is no copy
882 return $self->bail_on_events(@evts) if
883 $self->is_renewal and !$self->copy;
885 # Set some circ-specific flags in the script environment
886 my $evt = "environment";
887 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
889 if( $self->is_noncat ) {
890 $self->script_runner->insert("$evt.isNonCat", 1);
891 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
894 if( $self->is_precat ) {
895 $self->script_runner->insert("environment.isPrecat", 1, 1);
898 $self->script_runner->add_path( $_ ) for @$script_libs;
903 # --------------------------------------------------------------------------
904 # Does the circ permit work
905 # --------------------------------------------------------------------------
909 $self->log_me("do_permit()");
911 unless( $self->editor->requestor->id == $self->patron->id ) {
912 return $self->bail_on_events($self->editor->event)
913 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
916 $self->check_captured_holds();
917 $self->do_copy_checks();
918 return if $self->bail_out;
919 $self->run_patron_permit_scripts();
920 $self->run_copy_permit_scripts()
921 unless $self->is_precat or $self->is_noncat;
922 $self->check_item_deposit_events();
923 $self->override_events();
924 return if $self->bail_out;
926 if($self->is_precat and not $self->request_precat) {
929 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
930 return $self->bail_out(1) unless $self->is_renewal;
934 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
937 sub check_item_deposit_events {
939 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
940 if $self->is_deposit and not $self->is_deposit_exempt;
941 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
942 if $self->is_rental and not $self->is_rental_exempt;
945 # returns true if the user is not required to pay deposits
946 sub is_deposit_exempt {
948 my $pid = (ref $self->patron->profile) ?
949 $self->patron->profile->id : $self->patron->profile;
950 my $groups = $U->ou_ancestor_setting_value(
951 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
952 for my $grp (@$groups) {
953 return 1 if $self->is_group_descendant($grp, $pid);
958 # returns true if the user is not required to pay rental fees
959 sub is_rental_exempt {
961 my $pid = (ref $self->patron->profile) ?
962 $self->patron->profile->id : $self->patron->profile;
963 my $groups = $U->ou_ancestor_setting_value(
964 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
965 for my $grp (@$groups) {
966 return 1 if $self->is_group_descendant($grp, $pid);
971 sub is_group_descendant {
972 my($self, $p_id, $c_id) = @_;
973 return 0 unless defined $p_id and defined $c_id;
974 return 1 if $c_id == $p_id;
975 while(my $grp = $user_groups{$c_id}) {
976 $c_id = $grp->parent;
977 return 0 unless defined $c_id;
978 return 1 if $c_id == $p_id;
983 sub check_captured_holds {
985 my $copy = $self->copy;
986 my $patron = $self->patron;
988 return undef unless $copy;
990 my $s = $U->copy_status($copy->status)->id;
991 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
992 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
994 # Item is on the holds shelf, make sure it's going to the right person
995 my $hold = $self->editor->search_action_hold_request(
998 current_copy => $copy->id ,
999 capture_time => { '!=' => undef },
1000 cancel_time => undef,
1001 fulfillment_time => undef
1007 if ($hold and $hold->usr == $patron->id) {
1008 $self->checkout_is_for_hold(1);
1012 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1014 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1018 sub do_copy_checks {
1020 my $copy = $self->copy;
1021 return unless $copy;
1023 my $stat = $U->copy_status($copy->status)->id;
1025 # We cannot check out a copy if it is in-transit
1026 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1027 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1030 $self->handle_claims_returned();
1031 return if $self->bail_out;
1033 # no claims returned circ was found, check if there is any open circ
1034 unless( $self->is_renewal ) {
1036 my $circs = $self->editor->search_action_circulation(
1037 { target_copy => $copy->id, checkin_time => undef }
1040 if(my $old_circ = $circs->[0]) { # an open circ was found
1042 my $payload = {copy => $copy};
1044 if($old_circ->usr == $self->patron->id) {
1046 $payload->{old_circ} = $old_circ;
1048 # If there is an open circulation on the checkout item and an auto-renew
1049 # interval is defined, inform the caller that they should go
1050 # ahead and renew the item instead of warning about open circulations.
1052 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1054 'circ.checkout_auto_renew_age',
1058 if($auto_renew_intvl) {
1059 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1060 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1062 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1063 $payload->{auto_renew} = 1;
1068 return $self->bail_on_events(
1069 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1075 my $LEGACY_CIRC_EVENT_MAP = {
1076 'no_item' => 'ITEM_NOT_CATALOGED',
1077 'actor.usr.barred' => 'PATRON_BARRED',
1078 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1079 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1080 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1081 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1082 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1083 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1084 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1085 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1089 # ---------------------------------------------------------------------
1090 # This pushes any patron-related events into the list but does not
1091 # set bail_out for any events
1092 # ---------------------------------------------------------------------
1093 sub run_patron_permit_scripts {
1095 my $runner = $self->script_runner;
1096 my $patronid = $self->patron->id;
1100 if(!$self->legacy_script_support) {
1102 my $results = $self->run_indb_circ_test;
1103 unless($self->circ_test_success) {
1104 my @trimmed_results;
1106 if ($self->is_noncat) {
1107 # no_item result is OK during noncat checkout
1108 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1112 if ($self->checkout_is_for_hold) {
1113 # if this checkout will fulfill a hold, ignore CIRC blocks
1114 # and rely instead on the (later-checked) FULFILL block
1116 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1117 my $fblock_pens = $self->editor->search_config_standing_penalty(
1118 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1120 for my $res (@$results) {
1121 my $name = $res->{fail_part} || '';
1122 next if grep {$_->name eq $name} @$fblock_pens;
1123 push(@trimmed_results, $res);
1127 # not for hold or noncat
1128 @trimmed_results = @$results;
1132 # update the final set of test results
1133 $self->matrix_test_result(\@trimmed_results);
1135 push @allevents, $self->matrix_test_result_events;
1140 # ---------------------------------------------------------------------
1141 # # Now run the patron permit script
1142 # ---------------------------------------------------------------------
1143 $runner->load($self->circ_permit_patron);
1144 my $result = $runner->run or
1145 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1147 my $patron_events = $result->{events};
1149 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1150 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1151 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1152 $penalties = $penalties->{fatal_penalties};
1154 for my $pen (@$penalties) {
1155 # CIRC blocks are ignored if this is a FULFILL scenario
1156 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1157 my $event = OpenILS::Event->new($pen->name);
1158 $event->{desc} = $pen->label;
1159 push(@allevents, $event);
1162 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1166 $_->{payload} = $self->copy if
1167 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1170 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1172 $self->push_events(@allevents);
1175 sub matrix_test_result_codes {
1177 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1180 sub matrix_test_result_events {
1183 my $event = new OpenILS::Event(
1184 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1186 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1188 } (@{$self->matrix_test_result});
1191 sub run_indb_circ_test {
1193 return $self->matrix_test_result if $self->matrix_test_result;
1195 my $dbfunc = ($self->is_renewal) ?
1196 'action.item_user_renew_test' : 'action.item_user_circ_test';
1198 if( $self->is_precat && $self->request_precat) {
1199 $self->make_precat_copy;
1200 return if $self->bail_out;
1203 my $results = $self->editor->json_query(
1207 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1213 $self->circ_test_success($U->is_true($results->[0]->{success}));
1215 if(my $mp = $results->[0]->{matchpoint}) {
1216 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1217 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1218 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1219 if(defined($results->[0]->{renewals})) {
1220 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1222 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1223 if(defined($results->[0]->{grace_period})) {
1224 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1226 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1227 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1228 # Grab the *last* response for limit_groups, where it is more likely to be filled
1229 $self->limit_groups($results->[-1]->{limit_groups});
1232 return $self->matrix_test_result($results);
1235 # ---------------------------------------------------------------------
1236 # given a use and copy, this will calculate the circulation policy
1237 # parameters. Only works with in-db circ.
1238 # ---------------------------------------------------------------------
1242 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1244 $self->run_indb_circ_test;
1247 circ_test_success => $self->circ_test_success,
1248 failure_events => [],
1249 failure_codes => [],
1250 matchpoint => $self->circ_matrix_matchpoint
1253 unless($self->circ_test_success) {
1254 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1255 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1258 if($self->circ_matrix_matchpoint) {
1259 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1260 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1261 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1262 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1264 my $policy = $self->get_circ_policy(
1265 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1267 $$results{$_} = $$policy{$_} for keys %$policy;
1273 # ---------------------------------------------------------------------
1274 # Loads the circ policy info for duration, recurring fine, and max
1275 # fine based on the current copy
1276 # ---------------------------------------------------------------------
1277 sub get_circ_policy {
1278 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1281 duration_rule => $duration_rule->name,
1282 recurring_fine_rule => $recurring_fine_rule->name,
1283 max_fine_rule => $max_fine_rule->name,
1284 max_fine => $self->get_max_fine_amount($max_fine_rule),
1285 fine_interval => $recurring_fine_rule->recurrence_interval,
1286 renewal_remaining => $duration_rule->max_renewals,
1287 grace_period => $recurring_fine_rule->grace_period
1290 if($hard_due_date) {
1291 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1292 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1295 $policy->{duration_date_ceiling} = undef;
1296 $policy->{duration_date_ceiling_force} = undef;
1299 $policy->{duration} = $duration_rule->shrt
1300 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1301 $policy->{duration} = $duration_rule->normal
1302 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1303 $policy->{duration} = $duration_rule->extended
1304 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1306 $policy->{recurring_fine} = $recurring_fine_rule->low
1307 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1308 $policy->{recurring_fine} = $recurring_fine_rule->normal
1309 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1310 $policy->{recurring_fine} = $recurring_fine_rule->high
1311 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1316 sub get_max_fine_amount {
1318 my $max_fine_rule = shift;
1319 my $max_amount = $max_fine_rule->amount;
1321 # if is_percent is true then the max->amount is
1322 # use as a percentage of the copy price
1323 if ($U->is_true($max_fine_rule->is_percent)) {
1324 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1325 $max_amount = $price * $max_fine_rule->amount / 100;
1327 $U->ou_ancestor_setting_value(
1329 'circ.max_fine.cap_at_price',
1333 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1334 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1342 sub run_copy_permit_scripts {
1344 my $copy = $self->copy || return;
1345 my $runner = $self->script_runner;
1349 if(!$self->legacy_script_support) {
1350 my $results = $self->run_indb_circ_test;
1351 push @allevents, $self->matrix_test_result_events
1352 unless $self->circ_test_success;
1355 # ---------------------------------------------------------------------
1356 # Capture all of the copy permit events
1357 # ---------------------------------------------------------------------
1358 $runner->load($self->circ_permit_copy);
1359 my $result = $runner->run or
1360 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1361 my $copy_events = $result->{events};
1363 # ---------------------------------------------------------------------
1364 # Now collect all of the events together
1365 # ---------------------------------------------------------------------
1366 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1369 # See if this copy has an alert message
1370 my $ae = $self->check_copy_alert();
1371 push( @allevents, $ae ) if $ae;
1373 # uniquify the events
1374 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1375 @allevents = values %hash;
1377 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1379 $self->push_events(@allevents);
1383 sub check_copy_alert {
1385 return undef if $self->is_renewal;
1386 return OpenILS::Event->new(
1387 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1388 if $self->copy and $self->copy->alert_message;
1394 # --------------------------------------------------------------------------
1395 # If the call is overriding and has permissions to override every collected
1396 # event, the are cleared. Any event that the caller does not have
1397 # permission to override, will be left in the event list and bail_out will
1399 # XXX We need code in here to cancel any holds/transits on copies
1400 # that are being force-checked out
1401 # --------------------------------------------------------------------------
1402 sub override_events {
1404 my @events = @{$self->events};
1405 return unless @events;
1406 my $oargs = $self->override_args;
1408 if(!$self->override) {
1409 return $self->bail_out(1)
1410 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1415 for my $e (@events) {
1416 my $tc = $e->{textcode};
1417 next if $tc eq 'SUCCESS';
1418 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1419 my $ov = "$tc.override";
1420 $logger->info("circulator: attempting to override event: $ov");
1422 return $self->bail_on_events($self->editor->event)
1423 unless( $self->editor->allowed($ov) );
1425 return $self->bail_out(1);
1431 # --------------------------------------------------------------------------
1432 # If there is an open claimsreturn circ on the requested copy, close the
1433 # circ if overriding, otherwise bail out
1434 # --------------------------------------------------------------------------
1435 sub handle_claims_returned {
1437 my $copy = $self->copy;
1439 my $CR = $self->editor->search_action_circulation(
1441 target_copy => $copy->id,
1442 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1443 checkin_time => undef,
1447 return unless ($CR = $CR->[0]);
1451 # - If the caller has set the override flag, we will check the item in
1452 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1454 $CR->checkin_time('now');
1455 $CR->checkin_scan_time('now');
1456 $CR->checkin_lib($self->circ_lib);
1457 $CR->checkin_workstation($self->editor->requestor->wsid);
1458 $CR->checkin_staff($self->editor->requestor->id);
1460 $evt = $self->editor->event
1461 unless $self->editor->update_action_circulation($CR);
1464 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1467 $self->bail_on_events($evt) if $evt;
1472 # --------------------------------------------------------------------------
1473 # This performs the checkout
1474 # --------------------------------------------------------------------------
1478 $self->log_me("do_checkout()");
1480 # make sure perms are good if this isn't a renewal
1481 unless( $self->is_renewal ) {
1482 return $self->bail_on_events($self->editor->event)
1483 unless( $self->editor->allowed('COPY_CHECKOUT') );
1486 # verify the permit key
1487 unless( $self->check_permit_key ) {
1488 if( $self->permit_override ) {
1489 return $self->bail_on_events($self->editor->event)
1490 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1492 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1496 # if this is a non-cataloged circ, build the circ and finish
1497 if( $self->is_noncat ) {
1498 $self->checkout_noncat;
1500 OpenILS::Event->new('SUCCESS',
1501 payload => { noncat_circ => $self->circ }));
1505 if( $self->is_precat ) {
1506 $self->make_precat_copy;
1507 return if $self->bail_out;
1509 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1510 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1513 $self->do_copy_checks;
1514 return if $self->bail_out;
1516 $self->run_checkout_scripts();
1517 return if $self->bail_out;
1519 $self->build_checkout_circ_object();
1520 return if $self->bail_out;
1522 my $modify_to_start = $self->booking_adjusted_due_date();
1523 return if $self->bail_out;
1525 $self->apply_modified_due_date($modify_to_start);
1526 return if $self->bail_out;
1528 return $self->bail_on_events($self->editor->event)
1529 unless $self->editor->create_action_circulation($self->circ);
1531 # refresh the circ to force local time zone for now
1532 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1534 if($self->limit_groups) {
1535 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1538 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1540 return if $self->bail_out;
1542 $self->apply_deposit_fee();
1543 return if $self->bail_out;
1545 $self->handle_checkout_holds();
1546 return if $self->bail_out;
1548 # ------------------------------------------------------------------------------
1549 # Update the patron penalty info in the DB. Run it for permit-overrides
1550 # since the penalties are not updated during the permit phase
1551 # ------------------------------------------------------------------------------
1552 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1554 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1557 if($self->is_renewal) {
1558 # flesh the billing summary for the checked-in circ
1559 $pcirc = $self->editor->retrieve_action_circulation([
1561 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1566 OpenILS::Event->new('SUCCESS',
1568 copy => $U->unflesh_copy($self->copy),
1569 volume => $self->volume,
1570 circ => $self->circ,
1572 holds_fulfilled => $self->fulfilled_holds,
1573 deposit_billing => $self->deposit_billing,
1574 rental_billing => $self->rental_billing,
1575 parent_circ => $pcirc,
1576 patron => ($self->return_patron) ? $self->patron : undef,
1577 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1583 sub apply_deposit_fee {
1585 my $copy = $self->copy;
1587 ($self->is_deposit and not $self->is_deposit_exempt) or
1588 ($self->is_rental and not $self->is_rental_exempt);
1590 return if $self->is_deposit and $self->skip_deposit_fee;
1591 return if $self->is_rental and $self->skip_rental_fee;
1593 my $bill = Fieldmapper::money::billing->new;
1594 my $amount = $copy->deposit_amount;
1598 if($self->is_deposit) {
1599 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1601 $self->deposit_billing($bill);
1603 $billing_type = OILS_BILLING_TYPE_RENTAL;
1605 $self->rental_billing($bill);
1608 $bill->xact($self->circ->id);
1609 $bill->amount($amount);
1610 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1611 $bill->billing_type($billing_type);
1612 $bill->btype($btype);
1613 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1615 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1620 my $copy = $self->copy;
1622 my $stat = $copy->status if ref $copy->status;
1623 my $loc = $copy->location if ref $copy->location;
1624 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1626 $copy->status($stat->id) if $stat;
1627 $copy->location($loc->id) if $loc;
1628 $copy->circ_lib($circ_lib->id) if $circ_lib;
1629 $copy->editor($self->editor->requestor->id);
1630 $copy->edit_date('now');
1631 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1633 return $self->bail_on_events($self->editor->event)
1634 unless $self->editor->update_asset_copy($self->copy);
1636 $copy->status($U->copy_status($copy->status));
1637 $copy->location($loc) if $loc;
1638 $copy->circ_lib($circ_lib) if $circ_lib;
1641 sub update_reservation {
1643 my $reservation = $self->reservation;
1645 my $usr = $reservation->usr;
1646 my $target_rt = $reservation->target_resource_type;
1647 my $target_r = $reservation->target_resource;
1648 my $current_r = $reservation->current_resource;
1650 $reservation->usr($usr->id) if ref $usr;
1651 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1652 $reservation->target_resource($target_r->id) if ref $target_r;
1653 $reservation->current_resource($current_r->id) if ref $current_r;
1655 return $self->bail_on_events($self->editor->event)
1656 unless $self->editor->update_booking_reservation($self->reservation);
1659 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1660 $self->reservation($reservation);
1664 sub bail_on_events {
1665 my( $self, @evts ) = @_;
1666 $self->push_events(@evts);
1670 # ------------------------------------------------------------------------------
1671 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1672 # affects copies that will fulfill holds and CIRC affects all other copies.
1673 # If blocks exists, bail, push Events onto the event pile, and return true.
1674 # ------------------------------------------------------------------------------
1675 sub check_hold_fulfill_blocks {
1678 # See if the user has any penalties applied that prevent hold fulfillment
1679 my $pens = $self->editor->json_query({
1680 select => {csp => ['name', 'label']},
1681 from => {ausp => {csp => {}}},
1684 usr => $self->patron->id,
1685 org_unit => $U->get_org_full_path($self->circ_lib),
1687 {stop_date => undef},
1688 {stop_date => {'>' => 'now'}}
1691 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1695 return 0 unless @$pens;
1697 for my $pen (@$pens) {
1698 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1699 my $event = OpenILS::Event->new($pen->{name});
1700 $event->{desc} = $pen->{label};
1701 $self->push_events($event);
1704 $self->override_events;
1705 return $self->bail_out;
1709 # ------------------------------------------------------------------------------
1710 # When an item is checked out, see if we can fulfill a hold for this patron
1711 # ------------------------------------------------------------------------------
1712 sub handle_checkout_holds {
1714 my $copy = $self->copy;
1715 my $patron = $self->patron;
1717 my $e = $self->editor;
1718 $self->fulfilled_holds([]);
1720 # non-cats can't fulfill a hold
1721 return if $self->is_noncat;
1723 my $hold = $e->search_action_hold_request({
1724 current_copy => $copy->id ,
1725 cancel_time => undef,
1726 fulfillment_time => undef,
1728 {expire_time => undef},
1729 {expire_time => {'>' => 'now'}}
1733 if($hold and $hold->usr != $patron->id) {
1734 # reset the hold since the copy is now checked out
1736 $logger->info("circulator: un-targeting hold ".$hold->id.
1737 " because copy ".$copy->id." is getting checked out");
1739 $hold->clear_prev_check_time;
1740 $hold->clear_current_copy;
1741 $hold->clear_capture_time;
1742 $hold->clear_shelf_time;
1743 $hold->clear_shelf_expire_time;
1744 $hold->clear_current_shelf_lib;
1746 return $self->bail_on_event($e->event)
1747 unless $e->update_action_hold_request($hold);
1753 $hold = $self->find_related_user_hold($copy, $patron) or return;
1754 $logger->info("circulator: found related hold to fulfill in checkout");
1757 return if $self->check_hold_fulfill_blocks;
1759 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1761 # if the hold was never officially captured, capture it.
1762 $hold->current_copy($copy->id);
1763 $hold->capture_time('now') unless $hold->capture_time;
1764 $hold->fulfillment_time('now');
1765 $hold->fulfillment_staff($e->requestor->id);
1766 $hold->fulfillment_lib($self->circ_lib);
1768 return $self->bail_on_events($e->event)
1769 unless $e->update_action_hold_request($hold);
1771 $holdcode->delete_hold_copy_maps($e, $hold->id);
1772 return $self->fulfilled_holds([$hold->id]);
1776 # ------------------------------------------------------------------------------
1777 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1778 # the patron directly targets the checked out item, see if there is another hold
1779 # for the patron that could be fulfilled by the checked out item. Fulfill the
1780 # oldest hold and only fulfill 1 of them.
1782 # For "another hold":
1784 # First, check for one that the copy matches via hold_copy_map, ensuring that
1785 # *any* hold type that this copy could fill may end up filled.
1787 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1788 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1789 # that are non-requestable to count as capturing those hold types.
1790 # ------------------------------------------------------------------------------
1791 sub find_related_user_hold {
1792 my($self, $copy, $patron) = @_;
1793 my $e = $self->editor;
1795 # holds on precat copies are always copy-level, so this call will
1796 # always return undef. Exit early.
1797 return undef if $self->is_precat;
1799 return undef unless $U->ou_ancestor_setting_value(
1800 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1802 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1804 select => {ahr => ['id']},
1813 fkey => 'current_copy',
1814 type => 'left' # there may be no current_copy
1821 fulfillment_time => undef,
1822 cancel_time => undef,
1824 {expire_time => undef},
1825 {expire_time => {'>' => 'now'}}
1829 target_copy => $self->copy->id
1833 {id => undef}, # left-join copy may be nonexistent
1834 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1838 order_by => {ahr => {request_time => {direction => 'asc'}}},
1842 my $hold_info = $e->json_query($args)->[0];
1843 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1844 return undef if $U->ou_ancestor_setting_value(
1845 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1847 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1849 select => {ahr => ['id']},
1854 fkey => 'current_copy',
1855 type => 'left' # there may be no current_copy
1862 fulfillment_time => undef,
1863 cancel_time => undef,
1865 {expire_time => undef},
1866 {expire_time => {'>' => 'now'}}
1873 target => $self->volume->id
1879 target => $self->title->id
1885 {id => undef}, # left-join copy may be nonexistent
1886 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1890 order_by => {ahr => {request_time => {direction => 'asc'}}},
1894 $hold_info = $e->json_query($args)->[0];
1895 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1900 sub run_checkout_scripts {
1905 my $runner = $self->script_runner;
1914 my $hard_due_date_name;
1916 if(!$self->legacy_script_support) {
1917 $self->run_indb_circ_test();
1918 $duration = $self->circ_matrix_matchpoint->duration_rule;
1919 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1920 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1921 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1925 $runner->load($self->circ_duration);
1927 my $result = $runner->run or
1928 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1930 $duration_name = $result->{durationRule};
1931 $recurring_name = $result->{recurringFinesRule};
1932 $max_fine_name = $result->{maxFine};
1933 $hard_due_date_name = $result->{hardDueDate};
1936 $duration_name = $duration->name if $duration;
1937 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1940 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1941 return $self->bail_on_events($evt) if ($evt && !$nobail);
1943 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1944 return $self->bail_on_events($evt) if ($evt && !$nobail);
1946 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1947 return $self->bail_on_events($evt) if ($evt && !$nobail);
1949 if($hard_due_date_name) {
1950 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1951 return $self->bail_on_events($evt) if ($evt && !$nobail);
1957 # The item circulates with an unlimited duration
1961 $hard_due_date = undef;
1964 $self->duration_rule($duration);
1965 $self->recurring_fines_rule($recurring);
1966 $self->max_fine_rule($max_fine);
1967 $self->hard_due_date($hard_due_date);
1971 sub build_checkout_circ_object {
1974 my $circ = Fieldmapper::action::circulation->new;
1975 my $duration = $self->duration_rule;
1976 my $max = $self->max_fine_rule;
1977 my $recurring = $self->recurring_fines_rule;
1978 my $hard_due_date = $self->hard_due_date;
1979 my $copy = $self->copy;
1980 my $patron = $self->patron;
1981 my $duration_date_ceiling;
1982 my $duration_date_ceiling_force;
1986 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1987 $duration_date_ceiling = $policy->{duration_date_ceiling};
1988 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1990 my $dname = $duration->name;
1991 my $mname = $max->name;
1992 my $rname = $recurring->name;
1994 if($hard_due_date) {
1995 $hdname = $hard_due_date->name;
1998 $logger->debug("circulator: building circulation ".
1999 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
2001 $circ->duration($policy->{duration});
2002 $circ->recurring_fine($policy->{recurring_fine});
2003 $circ->duration_rule($duration->name);
2004 $circ->recurring_fine_rule($recurring->name);
2005 $circ->max_fine_rule($max->name);
2006 $circ->max_fine($policy->{max_fine});
2007 $circ->fine_interval($recurring->recurrence_interval);
2008 $circ->renewal_remaining($duration->max_renewals);
2009 $circ->grace_period($policy->{grace_period});
2013 $logger->info("circulator: copy found with an unlimited circ duration");
2014 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2015 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2016 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2017 $circ->renewal_remaining(0);
2018 $circ->grace_period(0);
2021 $circ->target_copy( $copy->id );
2022 $circ->usr( $patron->id );
2023 $circ->circ_lib( $self->circ_lib );
2024 $circ->workstation($self->editor->requestor->wsid)
2025 if defined $self->editor->requestor->wsid;
2027 # renewals maintain a link to the parent circulation
2028 $circ->parent_circ($self->parent_circ);
2030 if( $self->is_renewal ) {
2031 $circ->opac_renewal('t') if $self->opac_renewal;
2032 $circ->phone_renewal('t') if $self->phone_renewal;
2033 $circ->desk_renewal('t') if $self->desk_renewal;
2034 $circ->renewal_remaining($self->renewal_remaining);
2035 $circ->circ_staff($self->editor->requestor->id);
2039 # if the user provided an overiding checkout time,
2040 # (e.g. the checkout really happened several hours ago), then
2041 # we apply that here. Does this need a perm??
2042 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2043 if $self->checkout_time;
2045 # if a patron is renewing, 'requestor' will be the patron
2046 $circ->circ_staff($self->editor->requestor->id);
2047 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2052 sub do_reservation_pickup {
2055 $self->log_me("do_reservation_pickup()");
2057 $self->reservation->pickup_time('now');
2060 $self->reservation->current_resource &&
2061 $U->is_true($self->reservation->target_resource_type->catalog_item)
2063 # We used to try to set $self->copy and $self->patron here,
2064 # but that should already be done.
2066 $self->run_checkout_scripts(1);
2068 my $duration = $self->duration_rule;
2069 my $max = $self->max_fine_rule;
2070 my $recurring = $self->recurring_fines_rule;
2072 if ($duration && $max && $recurring) {
2073 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2075 my $dname = $duration->name;
2076 my $mname = $max->name;
2077 my $rname = $recurring->name;
2079 $logger->debug("circulator: updating reservation ".
2080 "with duration=$dname, maxfine=$mname, recurring=$rname");
2082 $self->reservation->fine_amount($policy->{recurring_fine});
2083 $self->reservation->max_fine($policy->{max_fine});
2084 $self->reservation->fine_interval($recurring->recurrence_interval);
2087 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2088 $self->update_copy();
2091 $self->reservation->fine_amount(
2092 $self->reservation->target_resource_type->fine_amount
2094 $self->reservation->max_fine(
2095 $self->reservation->target_resource_type->max_fine
2097 $self->reservation->fine_interval(
2098 $self->reservation->target_resource_type->fine_interval
2102 $self->update_reservation();
2105 sub do_reservation_return {
2107 my $request = shift;
2109 $self->log_me("do_reservation_return()");
2111 if (not ref $self->reservation) {
2112 my ($reservation, $evt) =
2113 $U->fetch_booking_reservation($self->reservation);
2114 return $self->bail_on_events($evt) if $evt;
2115 $self->reservation($reservation);
2118 $self->generate_fines(1);
2119 $self->reservation->return_time('now');
2120 $self->update_reservation();
2121 $self->reshelve_copy if $self->copy;
2123 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2124 $self->copy( $self->reservation->current_resource->catalog_item );
2128 sub booking_adjusted_due_date {
2130 my $circ = $self->circ;
2131 my $copy = $self->copy;
2133 return undef unless $self->use_booking;
2137 if( $self->due_date ) {
2139 return $self->bail_on_events($self->editor->event)
2140 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2142 $circ->due_date(cleanse_ISO8601($self->due_date));
2146 return unless $copy and $circ->due_date;
2149 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2150 if (@$booking_items) {
2151 my $booking_item = $booking_items->[0];
2152 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2154 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2155 my $shorten_circ_setting = $resource_type->elbow_room ||
2156 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2159 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2160 my $bookings = $booking_ses->request(
2161 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2162 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2164 $booking_ses->disconnect;
2166 my $dt_parser = DateTime::Format::ISO8601->new;
2167 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2169 for my $bid (@$bookings) {
2171 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2173 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2174 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2176 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2177 if ($booking_start < DateTime->now);
2180 if ($U->is_true($stop_circ_setting)) {
2181 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2183 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2184 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2187 # We set the circ duration here only to affect the logic that will
2188 # later (in a DB trigger) mangle the time part of the due date to
2189 # 11:59pm. Having any circ duration that is not a whole number of
2190 # days is enough to prevent the "correction."
2191 my $new_circ_duration = $due_date->epoch - time;
2192 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2193 $circ->duration("$new_circ_duration seconds");
2195 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2199 return $self->bail_on_events($self->editor->event)
2200 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2206 sub apply_modified_due_date {
2208 my $shift_earlier = shift;
2209 my $circ = $self->circ;
2210 my $copy = $self->copy;
2212 if( $self->due_date ) {
2214 return $self->bail_on_events($self->editor->event)
2215 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2217 $circ->due_date(cleanse_ISO8601($self->due_date));
2221 # if the due_date lands on a day when the location is closed
2222 return unless $copy and $circ->due_date;
2224 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2226 # due-date overlap should be determined by the location the item
2227 # is checked out from, not the owning or circ lib of the item
2228 my $org = $self->circ_lib;
2230 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2231 " with an item due date of ".$circ->due_date );
2233 my $dateinfo = $U->storagereq(
2234 'open-ils.storage.actor.org_unit.closed_date.overlap',
2235 $org, $circ->due_date );
2238 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2239 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2241 # XXX make the behavior more dynamic
2242 # for now, we just push the due date to after the close date
2243 if ($shift_earlier) {
2244 $circ->due_date($dateinfo->{start});
2246 $circ->due_date($dateinfo->{end});
2254 sub create_due_date {
2255 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2257 # if there is a raw time component (e.g. from postgres),
2258 # turn it into an interval that interval_to_seconds can parse
2259 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2261 # for now, use the server timezone. TODO: use workstation org timezone
2262 my $due_date = DateTime->now(time_zone => 'local');
2263 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2265 # add the circ duration
2266 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2269 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2270 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2271 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2276 # return ISO8601 time with timezone
2277 return $due_date->strftime('%FT%T%z');
2282 sub make_precat_copy {
2284 my $copy = $self->copy;
2287 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2289 $copy->editor($self->editor->requestor->id);
2290 $copy->edit_date('now');
2291 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2292 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2293 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2294 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2295 $self->update_copy();
2299 $logger->info("circulator: Creating a new precataloged ".
2300 "copy in checkout with barcode " . $self->copy_barcode);
2302 $copy = Fieldmapper::asset::copy->new;
2303 $copy->circ_lib($self->circ_lib);
2304 $copy->creator($self->editor->requestor->id);
2305 $copy->editor($self->editor->requestor->id);
2306 $copy->barcode($self->copy_barcode);
2307 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2308 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2309 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2311 $copy->dummy_title($self->dummy_title || "");
2312 $copy->dummy_author($self->dummy_author || "");
2313 $copy->dummy_isbn($self->dummy_isbn || "");
2314 $copy->circ_modifier($self->circ_modifier);
2317 # See if we need to override the circ_lib for the copy with a configured circ_lib
2318 # Setting is shortname of the org unit
2319 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2320 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2322 if($precat_circ_lib) {
2323 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2326 $self->bail_on_events($self->editor->event);
2330 $copy->circ_lib($org->id);
2334 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2336 $self->push_events($self->editor->event);
2340 # this is a little bit of a hack, but we need to
2341 # get the copy into the script runner
2342 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2346 sub checkout_noncat {
2352 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2353 my $count = $self->noncat_count || 1;
2354 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2356 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2360 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2361 $self->editor->requestor->id,
2369 $self->push_events($evt);
2377 # If a copy goes into transit and is then checked in before the transit checkin
2378 # interval has expired, push an event onto the overridable events list.
2379 sub check_transit_checkin_interval {
2382 # only concerned with in-transit items
2383 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2385 # no interval, no problem
2386 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2387 return unless $interval;
2389 # capture the transit so we don't have to fetch it again later during checkin
2391 $self->editor->search_action_transit_copy(
2392 {target_copy => $self->copy->id, dest_recv_time => undef}
2396 # transit from X to X for whatever reason has no min interval
2397 return if $self->transit->source == $self->transit->dest;
2399 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2400 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2401 my $horizon = $t_start->add(seconds => $seconds);
2403 # See if we are still within the transit checkin forbidden range
2404 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2405 if $horizon > DateTime->now;
2408 # Retarget local holds at checkin
2409 sub checkin_retarget {
2411 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2412 return unless $self->is_checkin; # Renewals need not be checked
2413 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2414 return if $self->is_precat; # No holds for precats
2415 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2416 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2417 my $status = $U->copy_status($self->copy->status);
2418 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2419 # Specifically target items that are likely new (by status ID)
2420 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2421 my $location = $self->copy->location;
2422 if(!ref($location)) {
2423 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2424 $self->copy->location($location);
2426 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2428 # Fetch holds for the bib
2429 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2430 $self->editor->authtoken,
2433 capture_time => undef, # No touching captured holds
2434 frozen => 'f', # Don't bother with frozen holds
2435 pickup_lib => $self->circ_lib # Only holds actually here
2438 # Error? Skip the step.
2439 return if exists $result->{"ilsevent"};
2443 foreach my $holdlist (keys %{$result}) {
2444 push @$holds, @{$result->{$holdlist}};
2447 return if scalar(@$holds) == 0; # No holds, no retargeting
2449 # Check for parts on this copy
2450 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2451 my %parts_hash = ();
2452 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2454 # Loop over holds in request-ish order
2455 # Stage 1: Get them into request-ish order
2456 # Also grab type and target for skipping low hanging ones
2457 $result = $self->editor->json_query({
2458 "select" => { "ahr" => ["id", "hold_type", "target"] },
2459 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2460 "where" => { "id" => $holds },
2462 { "class" => "pgt", "field" => "hold_priority"},
2463 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2464 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2465 { "class" => "ahr", "field" => "request_time"}
2470 if (ref $result eq "ARRAY" and scalar @$result) {
2471 foreach (@{$result}) {
2472 # Copy level, but not this copy?
2473 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2474 and $_->{target} != $self->copy->id);
2475 # Volume level, but not this volume?
2476 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2477 if(@$parts) { # We have parts?
2479 next if ($_->{hold_type} eq 'T');
2480 # Skip part holds for parts not on this copy
2481 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2483 # No parts, no part holds
2484 next if ($_->{hold_type} eq 'P');
2486 # So much for easy stuff, attempt a retarget!
2487 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2488 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2489 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2497 $self->log_me("do_checkin()");
2499 return $self->bail_on_events(
2500 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2503 $self->check_transit_checkin_interval;
2504 $self->checkin_retarget;
2506 # the renew code and mk_env should have already found our circulation object
2507 unless( $self->circ ) {
2509 my $circs = $self->editor->search_action_circulation(
2510 { target_copy => $self->copy->id, checkin_time => undef });
2512 $self->circ($$circs[0]);
2514 # for now, just warn if there are multiple open circs on a copy
2515 $logger->warn("circulator: we have ".scalar(@$circs).
2516 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2519 # run the fine generator against this circ, if this circ is there
2520 $self->generate_fines_start if $self->circ;
2522 if( $self->checkin_check_holds_shelf() ) {
2523 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2524 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2525 if($self->fake_hold_dest) {
2526 $self->hold->pickup_lib($self->circ_lib);
2528 $self->checkin_flesh_events;
2532 unless( $self->is_renewal ) {
2533 return $self->bail_on_events($self->editor->event)
2534 unless $self->editor->allowed('COPY_CHECKIN');
2537 $self->push_events($self->check_copy_alert());
2538 $self->push_events($self->check_checkin_copy_status());
2540 # if the circ is marked as 'claims returned', add the event to the list
2541 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2542 if ($self->circ and $self->circ->stop_fines
2543 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2545 $self->check_circ_deposit();
2547 # handle the overridable events
2548 $self->override_events unless $self->is_renewal;
2549 return if $self->bail_out;
2551 if( $self->copy and !$self->transit ) {
2553 $self->editor->search_action_transit_copy(
2554 { target_copy => $self->copy->id, dest_recv_time => undef }
2560 $self->generate_fines_finish;
2561 $self->checkin_handle_circ;
2562 return if $self->bail_out;
2563 $self->checkin_changed(1);
2565 } elsif( $self->transit ) {
2566 my $hold_transit = $self->process_received_transit;
2567 $self->checkin_changed(1);
2569 if( $self->bail_out ) {
2570 $self->checkin_flesh_events;
2574 if( my $e = $self->check_checkin_copy_status() ) {
2575 # If the original copy status is special, alert the caller
2576 my $ev = $self->events;
2577 $self->events([$e]);
2578 $self->override_events;
2579 return if $self->bail_out;
2583 if( $hold_transit or
2584 $U->copy_status($self->copy->status)->id
2585 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2588 if( $hold_transit ) {
2589 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2591 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2596 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2598 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2599 $self->reshelve_copy(1);
2600 $self->cancelled_hold_transit(1);
2601 $self->notify_hold(0); # don't notify for cancelled holds
2602 $self->fake_hold_dest(0);
2603 return if $self->bail_out;
2605 } elsif ($hold and $hold->hold_type eq 'R') {
2607 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2608 $self->notify_hold(0); # No need to notify
2609 $self->fake_hold_dest(0);
2610 $self->noop(1); # Don't try and capture for other holds/transits now
2611 $self->update_copy();
2612 $hold->fulfillment_time('now');
2613 $self->bail_on_events($self->editor->event)
2614 unless $self->editor->update_action_hold_request($hold);
2618 # hold transited to correct location
2619 if($self->fake_hold_dest) {
2620 $hold->pickup_lib($self->circ_lib);
2622 $self->checkin_flesh_events;
2627 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2629 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2630 " that is in-transit, but there is no transit.. repairing");
2631 $self->reshelve_copy(1);
2632 return if $self->bail_out;
2635 if( $self->is_renewal ) {
2636 $self->finish_fines_and_voiding;
2637 return if $self->bail_out;
2638 $self->push_events(OpenILS::Event->new('SUCCESS'));
2642 # ------------------------------------------------------------------------------
2643 # Circulations and transits are now closed where necessary. Now go on to see if
2644 # this copy can fulfill a hold or needs to be routed to a different location
2645 # ------------------------------------------------------------------------------
2647 my $needed_for_something = 0; # formerly "needed_for_hold"
2649 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2651 if (!$self->remote_hold) {
2652 if ($self->use_booking) {
2653 my $potential_hold = $self->hold_capture_is_possible;
2654 my $potential_reservation = $self->reservation_capture_is_possible;
2656 if ($potential_hold and $potential_reservation) {
2657 $logger->info("circulator: item could fulfill either hold or reservation");
2658 $self->push_events(new OpenILS::Event(
2659 "HOLD_RESERVATION_CONFLICT",
2660 "hold" => $potential_hold,
2661 "reservation" => $potential_reservation
2663 return if $self->bail_out;
2664 } elsif ($potential_hold) {
2665 $needed_for_something =
2666 $self->attempt_checkin_hold_capture;
2667 } elsif ($potential_reservation) {
2668 $needed_for_something =
2669 $self->attempt_checkin_reservation_capture;
2672 $needed_for_something = $self->attempt_checkin_hold_capture;
2675 return if $self->bail_out;
2677 unless($needed_for_something) {
2678 my $circ_lib = (ref $self->copy->circ_lib) ?
2679 $self->copy->circ_lib->id : $self->copy->circ_lib;
2681 if( $self->remote_hold ) {
2682 $circ_lib = $self->remote_hold->pickup_lib;
2683 $logger->warn("circulator: Copy ".$self->copy->barcode.
2684 " is on a remote hold's shelf, sending to $circ_lib");
2687 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2689 my $suppress_transit = 0;
2691 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2692 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2693 if($suppress_transit_source && $suppress_transit_source->{value}) {
2694 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2695 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2696 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2697 $suppress_transit = 1;
2702 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2703 # copy is where it needs to be, either for hold or reshelving
2705 $self->checkin_handle_precat();
2706 return if $self->bail_out;
2709 # copy needs to transit "home", or stick here if it's a floating copy
2711 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2712 my $res = $self->editor->json_query(
2714 'evergreen.can_float',
2715 $self->copy->floating->id,
2716 $self->copy->circ_lib,
2721 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2723 if ($can_float) { # Yep, floating, stick here
2724 $self->checkin_changed(1);
2725 $self->copy->circ_lib( $self->circ_lib );
2728 my $bc = $self->copy->barcode;
2729 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2730 $self->checkin_build_copy_transit($circ_lib);
2731 return if $self->bail_out;
2732 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2736 } else { # no-op checkin
2737 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2738 $self->checkin_changed(1);
2739 $self->copy->circ_lib( $self->circ_lib );
2744 if($self->claims_never_checked_out and
2745 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2747 # the item was not supposed to be checked out to the user and should now be marked as missing
2748 $self->copy->status(OILS_COPY_STATUS_MISSING);
2752 $self->reshelve_copy unless $needed_for_something;
2755 return if $self->bail_out;
2757 unless($self->checkin_changed) {
2759 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2760 my $stat = $U->copy_status($self->copy->status)->id;
2762 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2763 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2764 $self->bail_out(1); # no need to commit anything
2768 $self->push_events(OpenILS::Event->new('SUCCESS'))
2769 unless @{$self->events};
2772 $self->finish_fines_and_voiding;
2774 OpenILS::Utils::Penalty->calculate_penalties(
2775 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2777 $self->checkin_flesh_events;
2781 sub finish_fines_and_voiding {
2783 return unless $self->circ;
2785 # gather any updates to the circ after fine generation, if there was a circ
2786 $self->generate_fines_finish;
2788 return unless $self->backdate or $self->void_overdues;
2790 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2791 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2793 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2794 $self->editor, $self->circ, $self->backdate, $note);
2796 return $self->bail_on_events($evt) if $evt;
2798 # Make sure the circ is open or closed as necessary.
2799 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2800 return $self->bail_on_events($evt) if $evt;
2806 # if a deposit was payed for this item, push the event
2807 sub check_circ_deposit {
2809 return unless $self->circ;
2810 my $deposit = $self->editor->search_money_billing(
2812 xact => $self->circ->id,
2814 }, {idlist => 1})->[0];
2816 $self->push_events(OpenILS::Event->new(
2817 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2822 my $force = $self->force || shift;
2823 my $copy = $self->copy;
2825 my $stat = $U->copy_status($copy->status)->id;
2828 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2829 $stat != OILS_COPY_STATUS_CATALOGING and
2830 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2831 $stat != OILS_COPY_STATUS_RESHELVING )) {
2833 $copy->status( OILS_COPY_STATUS_RESHELVING );
2835 $self->checkin_changed(1);
2840 # Returns true if the item is at the current location
2841 # because it was transited there for a hold and the
2842 # hold has not been fulfilled
2843 sub checkin_check_holds_shelf {
2845 return 0 unless $self->copy;
2848 $U->copy_status($self->copy->status)->id ==
2849 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2851 # Attempt to clear shelf expired holds for this copy
2852 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2853 if($self->clear_expired);
2855 # find the hold that put us on the holds shelf
2856 my $holds = $self->editor->search_action_hold_request(
2858 current_copy => $self->copy->id,
2859 capture_time => { '!=' => undef },
2860 fulfillment_time => undef,
2861 cancel_time => undef,
2866 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2867 $self->reshelve_copy(1);
2871 my $hold = $$holds[0];
2873 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2874 $hold->id. "] for copy ".$self->copy->barcode);
2876 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2877 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2878 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2879 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2880 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2881 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2882 $self->fake_hold_dest(1);
2888 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2889 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2893 $logger->info("circulator: hold is not for here..");
2894 $self->remote_hold($hold);
2899 sub checkin_handle_precat {
2901 my $copy = $self->copy;
2903 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2904 $copy->status(OILS_COPY_STATUS_CATALOGING);
2905 $self->update_copy();
2906 $self->checkin_changed(1);
2907 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2912 sub checkin_build_copy_transit {
2915 my $copy = $self->copy;
2916 my $transit = Fieldmapper::action::transit_copy->new;
2918 # if we are transiting an item to the shelf shelf, it's a hold transit
2919 if (my $hold = $self->remote_hold) {
2920 $transit = Fieldmapper::action::hold_transit_copy->new;
2921 $transit->hold($hold->id);
2923 # the item is going into transit, remove any shelf-iness
2924 if ($hold->current_shelf_lib or $hold->shelf_time) {
2925 $hold->clear_current_shelf_lib;
2926 $hold->clear_shelf_time;
2927 return $self->bail_on_events($self->editor->event)
2928 unless $self->editor->update_action_hold_request($hold);
2932 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2933 $logger->info("circulator: transiting copy to $dest");
2935 $transit->source($self->circ_lib);
2936 $transit->dest($dest);
2937 $transit->target_copy($copy->id);
2938 $transit->source_send_time('now');
2939 $transit->copy_status( $U->copy_status($copy->status)->id );
2941 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2943 if ($self->remote_hold) {
2944 return $self->bail_on_events($self->editor->event)
2945 unless $self->editor->create_action_hold_transit_copy($transit);
2947 return $self->bail_on_events($self->editor->event)
2948 unless $self->editor->create_action_transit_copy($transit);
2951 # ensure the transit is returned to the caller
2952 $self->transit($transit);
2954 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2956 $self->checkin_changed(1);
2960 sub hold_capture_is_possible {
2962 my $copy = $self->copy;
2964 # we've been explicitly told not to capture any holds
2965 return 0 if $self->capture eq 'nocapture';
2967 # See if this copy can fulfill any holds
2968 my $hold = $holdcode->find_nearest_permitted_hold(
2969 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2971 return undef if ref $hold eq "HASH" and
2972 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2976 sub reservation_capture_is_possible {
2978 my $copy = $self->copy;
2980 # we've been explicitly told not to capture any holds
2981 return 0 if $self->capture eq 'nocapture';
2983 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2984 my $resv = $booking_ses->request(
2985 "open-ils.booking.reservations.could_capture",
2986 $self->editor->authtoken, $copy->barcode
2988 $booking_ses->disconnect;
2989 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2990 $self->push_events($resv);
2996 # returns true if the item was used (or may potentially be used
2997 # in subsequent calls) to capture a hold.
2998 sub attempt_checkin_hold_capture {
3000 my $copy = $self->copy;
3002 # we've been explicitly told not to capture any holds
3003 return 0 if $self->capture eq 'nocapture';
3005 # See if this copy can fulfill any holds
3006 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3007 $self->editor, $copy, $self->editor->requestor );
3010 $logger->debug("circulator: no potential permitted".
3011 "holds found for copy ".$copy->barcode);
3015 if($self->capture ne 'capture') {
3016 # see if this item is in a hold-capture-delay location
3017 my $location = $self->copy->location;
3018 if(!ref($location)) {
3019 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3020 $self->copy->location($location);
3022 if($U->is_true($location->hold_verify)) {
3023 $self->bail_on_events(
3024 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3029 $self->retarget($retarget);
3031 my $suppress_transit = 0;
3032 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3033 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3034 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3035 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3036 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3037 $suppress_transit = 1;
3038 $hold->pickup_lib($self->circ_lib);
3043 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3045 $hold->current_copy($copy->id);
3046 $hold->capture_time('now');
3047 $self->put_hold_on_shelf($hold)
3048 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3050 # prevent DB errors caused by fetching
3051 # holds from storage, and updating through cstore
3052 $hold->clear_fulfillment_time;
3053 $hold->clear_fulfillment_staff;
3054 $hold->clear_fulfillment_lib;
3055 $hold->clear_expire_time;
3056 $hold->clear_cancel_time;
3057 $hold->clear_prev_check_time unless $hold->prev_check_time;
3059 $self->bail_on_events($self->editor->event)
3060 unless $self->editor->update_action_hold_request($hold);
3062 $self->checkin_changed(1);
3064 return 0 if $self->bail_out;
3066 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3068 if ($hold->hold_type eq 'R') {
3069 $copy->status(OILS_COPY_STATUS_CATALOGING);
3070 $hold->fulfillment_time('now');
3071 $self->noop(1); # Block other transit/hold checks
3072 $self->bail_on_events($self->editor->event)
3073 unless $self->editor->update_action_hold_request($hold);
3075 # This hold was captured in the correct location
3076 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3077 $self->push_events(OpenILS::Event->new('SUCCESS'));
3079 #$self->do_hold_notify($hold->id);
3080 $self->notify_hold($hold->id);
3085 # Hold needs to be picked up elsewhere. Build a hold
3086 # transit and route the item.
3087 $self->checkin_build_hold_transit();
3088 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3089 return 0 if $self->bail_out;
3090 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3093 # make sure we save the copy status
3095 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3099 sub attempt_checkin_reservation_capture {
3101 my $copy = $self->copy;
3103 # we've been explicitly told not to capture any holds
3104 return 0 if $self->capture eq 'nocapture';
3106 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3107 my $evt = $booking_ses->request(
3108 "open-ils.booking.resources.capture_for_reservation",
3109 $self->editor->authtoken,
3111 1 # don't update copy - we probably have it locked
3113 $booking_ses->disconnect;
3115 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3117 "open-ils.booking.resources.capture_for_reservation " .
3118 "didn't return an event!"
3122 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3123 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3125 # not-transferable is an error event we'll pass on the user
3126 $logger->warn("reservation capture attempted against non-transferable item");
3127 $self->push_events($evt);
3129 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3130 # Re-retrieve copy as reservation capture may have changed
3131 # its status and whatnot.
3133 "circulator: booking capture win on copy " . $self->copy->id
3135 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3137 "circulator: changing copy " . $self->copy->id .
3138 "'s status from " . $self->copy->status . " to " .
3141 $self->copy->status($new_copy_status);
3144 $self->reservation($evt->{"payload"}->{"reservation"});
3146 if (exists $evt->{"payload"}->{"transit"}) {
3150 "org" => $evt->{"payload"}->{"transit"}->dest
3154 $self->checkin_changed(1);
3158 # other results are treated as "nothing to capture"
3162 sub do_hold_notify {
3163 my( $self, $holdid ) = @_;
3165 my $e = new_editor(xact => 1);
3166 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3168 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3169 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3171 $logger->info("circulator: running delayed hold notify process");
3173 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3174 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3176 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3177 hold_id => $holdid, requestor => $self->editor->requestor);
3179 $logger->debug("circulator: built hold notifier");
3181 if(!$notifier->event) {
3183 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3185 my $stat = $notifier->send_email_notify;
3186 if( $stat == '1' ) {
3187 $logger->info("circulator: hold notify succeeded for hold $holdid");
3191 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3194 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3198 sub retarget_holds {
3200 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3201 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3202 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3203 # no reason to wait for the return value
3207 sub checkin_build_hold_transit {
3210 my $copy = $self->copy;
3211 my $hold = $self->hold;
3212 my $trans = Fieldmapper::action::hold_transit_copy->new;
3214 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3216 $trans->hold($hold->id);
3217 $trans->source($self->circ_lib);
3218 $trans->dest($hold->pickup_lib);
3219 $trans->source_send_time("now");
3220 $trans->target_copy($copy->id);
3222 # when the copy gets to its destination, it will recover
3223 # this status - put it onto the holds shelf
3224 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3226 return $self->bail_on_events($self->editor->event)
3227 unless $self->editor->create_action_hold_transit_copy($trans);
3232 sub process_received_transit {
3234 my $copy = $self->copy;
3235 my $copyid = $self->copy->id;
3237 my $status_name = $U->copy_status($copy->status)->name;
3238 $logger->debug("circulator: attempting transit receive on ".
3239 "copy $copyid. Copy status is $status_name");
3241 my $transit = $self->transit;
3243 # Check if we are in a transit suppress range
3244 my $suppress_transit = 0;
3245 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3246 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3247 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3248 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3249 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3250 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3251 $suppress_transit = 1;
3252 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3256 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3257 # - this item is in-transit to a different location
3258 # - Or we are capturing holds as transits, so why create a new transit?
3260 my $tid = $transit->id;
3261 my $loc = $self->circ_lib;
3262 my $dest = $transit->dest;
3264 $logger->info("circulator: Fowarding transit on copy which is destined ".
3265 "for a different location. transit=$tid, copy=$copyid, current ".
3266 "location=$loc, destination location=$dest");
3268 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3270 # grab the associated hold object if available
3271 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3272 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3274 return $self->bail_on_events($evt);
3277 # The transit is received, set the receive time
3278 $transit->dest_recv_time('now');
3279 $self->bail_on_events($self->editor->event)
3280 unless $self->editor->update_action_transit_copy($transit);
3282 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3284 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3285 $copy->status( $transit->copy_status );
3286 $self->update_copy();
3287 return if $self->bail_out;
3291 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3294 # hold has arrived at destination, set shelf time
3295 $self->put_hold_on_shelf($hold);
3296 $self->bail_on_events($self->editor->event)
3297 unless $self->editor->update_action_hold_request($hold);
3298 return if $self->bail_out;
3300 $self->notify_hold($hold_transit->hold);
3303 $hold_transit = undef;
3304 $self->cancelled_hold_transit(1);
3305 $self->reshelve_copy(1);
3306 $self->fake_hold_dest(0);
3311 OpenILS::Event->new(
3314 payload => { transit => $transit, holdtransit => $hold_transit } ));
3316 return $hold_transit;
3320 # ------------------------------------------------------------------
3321 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3322 # ------------------------------------------------------------------
3323 sub put_hold_on_shelf {
3324 my($self, $hold) = @_;
3325 $hold->shelf_time('now');
3326 $hold->current_shelf_lib($self->circ_lib);
3327 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3333 sub generate_fines {
3335 my $reservation = shift;
3337 $self->generate_fines_start($reservation);
3338 $self->generate_fines_finish($reservation);
3343 sub generate_fines_start {
3345 my $reservation = shift;
3346 my $dt_parser = DateTime::Format::ISO8601->new;
3348 my $obj = $reservation ? $self->reservation : $self->circ;
3350 # If we have a grace period
3351 if($obj->can('grace_period')) {
3352 # Parse out the due date
3353 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3354 # Add the grace period to the due date
3355 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3356 # Don't generate fines on circs still in grace period
3357 return undef if ($due_date > DateTime->now);
3360 if (!exists($self->{_gen_fines_req})) {
3361 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3363 'open-ils.storage.action.circulation.overdue.generate_fines',
3371 sub generate_fines_finish {
3373 my $reservation = shift;
3375 return undef unless $self->{_gen_fines_req};
3377 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3379 $self->{_gen_fines_req}->wait_complete;
3380 delete($self->{_gen_fines_req});
3382 # refresh the circ in case the fine generator set the stop_fines field
3383 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3384 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3389 sub checkin_handle_circ {
3391 my $circ = $self->circ;
3392 my $copy = $self->copy;
3396 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3398 # backdate the circ if necessary
3399 if($self->backdate) {
3400 my $evt = $self->checkin_handle_backdate;
3401 return $self->bail_on_events($evt) if $evt;
3404 if(!$circ->stop_fines) {
3405 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3406 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3407 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3408 $circ->stop_fines_time('now');
3409 $circ->stop_fines_time($self->backdate) if $self->backdate;
3412 # Set the checkin vars since we have the item
3413 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3415 # capture the true scan time for back-dated checkins
3416 $circ->checkin_scan_time('now');
3418 $circ->checkin_staff($self->editor->requestor->id);
3419 $circ->checkin_lib($self->circ_lib);
3420 $circ->checkin_workstation($self->editor->requestor->wsid);
3422 my $circ_lib = (ref $self->copy->circ_lib) ?
3423 $self->copy->circ_lib->id : $self->copy->circ_lib;
3424 my $stat = $U->copy_status($self->copy->status)->id;
3426 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3427 # we will now handle lost fines, but the copy will retain its 'lost'
3428 # status if it needs to transit home unless lost_immediately_available
3431 # if we decide to also delay fine handling until the item arrives home,
3432 # we will need to call lost fine handling code both when checking items
3433 # in and also when receiving transits
3434 $self->checkin_handle_lost($circ_lib);
3435 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3436 # same process as above.
3437 $self->checkin_handle_long_overdue($circ_lib);
3438 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3439 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3441 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3446 # see if there are any fines owed on this circ. if not, close it
3447 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3448 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3450 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3452 return $self->bail_on_events($self->editor->event)
3453 unless $self->editor->update_action_circulation($circ);
3458 # ------------------------------------------------------------------
3459 # See if we need to void billings, etc. for lost checkin
3460 # ------------------------------------------------------------------
3461 sub checkin_handle_lost {
3463 my $circ_lib = shift;
3465 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3466 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3468 return $self->checkin_handle_lost_or_longoverdue(
3469 circ_lib => $circ_lib,
3470 max_return => $max_return,
3471 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3472 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3473 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3474 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3475 ous_use_last_activity => undef, # not supported for LOST checkin
3476 void_cost_btype => 3,
3481 # ------------------------------------------------------------------
3482 # See if we need to void billings, etc. for long-overdue checkin
3483 # note: not using constants below since they serve little purpose
3484 # for single-use strings that are descriptive in their own right
3485 # and mostly just complicate debugging.
3486 # ------------------------------------------------------------------
3487 sub checkin_handle_long_overdue {
3489 my $circ_lib = shift;
3491 $logger->info("circulator: processing long-overdue checkin...");
3493 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3494 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3496 return $self->checkin_handle_lost_or_longoverdue(
3497 circ_lib => $circ_lib,
3498 max_return => $max_return,
3499 is_longoverdue => 1,
3500 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3501 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3502 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3503 ous_immediately_available => 'circ.longoverdue_immediately_available',
3504 ous_use_last_activity =>
3505 'circ.longoverdue.use_last_activity_date_on_return',
3506 void_cost_btype => 10,
3507 void_fee_btype => 11
3511 # last billing activity is last payment time, last billing time, or the
3512 # circ due date. If the relevant "use last activity" org unit setting is
3513 # false/unset, then last billing activity is always the due date.
3514 sub get_circ_last_billing_activity {
3516 my $circ_lib = shift;
3517 my $setting = shift;
3518 my $date = $self->circ->due_date;
3520 return $date unless $setting and
3521 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3523 my $xact = $self->editor->retrieve_money_billable_transaction([
3525 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3528 if ($xact->summary) {
3529 $date = $xact->summary->last_payment_ts ||
3530 $xact->summary->last_billing_ts ||
3531 $self->circ->due_date;
3538 sub checkin_handle_lost_or_longoverdue {
3539 my ($self, %args) = @_;
3541 my $circ = $self->circ;
3542 my $max_return = $args{max_return};
3543 my $circ_lib = $args{circ_lib};
3548 $self->get_circ_last_billing_activity(
3549 $circ_lib, $args{ous_use_last_activity});
3552 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3553 $tm[5] -= 1 if $tm[5] > 0;
3554 my $due = timelocal(int($tm[1]), int($tm[2]),
3555 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3558 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3560 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3561 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3562 "DUE: $due LAST: $last_chance");
3564 $max_return = 0 if $today < $last_chance;
3570 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3571 "return interval. skipping fine/fee voiding, etc.");
3573 } else { # within max-return interval or no interval defined
3575 $logger->info("circulator: check-in of lost/lo item is within the ".
3576 "max return interval (or no interval is defined). Proceeding ".
3577 "with fine/fee voiding, etc.");
3579 my $void_cost = $U->ou_ancestor_setting_value(
3580 $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3581 my $void_proc_fee = $U->ou_ancestor_setting_value(
3582 $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3583 my $restore_od = $U->ou_ancestor_setting_value(
3584 $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3586 # for reference: generate-overdues-on-long-overdue-checkin is not
3587 # supported because it doesn't make any sense that a circ would be
3588 # marked as long-overdue before it was done being regular-overdue
3589 if (!$args{is_longoverdue}) {
3590 $self->generate_lost_overdue(1) if
3591 $U->ou_ancestor_setting_value($circ_lib,
3592 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
3596 $self->checkin_handle_lost_or_lo_now_found(
3597 $args{void_cost_btype}, $args{is_longoverdue}) if $void_cost;
3598 $self->checkin_handle_lost_or_lo_now_found(
3599 $args{void_fee_btype}, $args{is_longoverdue}) if $void_proc_fee;
3600 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3601 if $restore_od && ! $self->void_overdues;
3604 if ($circ_lib != $self->circ_lib) {
3605 # if the item is not home, check to see if we want to retain the
3606 # lost/longoverdue status at this point in the process
3608 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3609 $args{ous_immediately_available}, $self->editor) || 0;
3611 if ($immediately_available) {
3612 # item status does not need to be retained, so give it a
3613 # reshelving status as if it were a normal checkin
3614 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3617 $logger->info("circulator: leaving lost/longoverdue copy".
3618 " status in place on checkin");
3621 # lost/longoverdue item is home and processed, treat like a normal
3622 # checkin from this point on
3623 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3629 sub checkin_handle_backdate {
3632 # ------------------------------------------------------------------
3633 # clean up the backdate for date comparison
3634 # XXX We are currently taking the due-time from the original due-date,
3635 # not the input. Do we need to do this? This certainly interferes with
3636 # backdating of hourly checkouts, but that is likely a very rare case.
3637 # ------------------------------------------------------------------
3638 my $bd = cleanse_ISO8601($self->backdate);
3639 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3640 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3641 $new_date->set_hour($original_date->hour());
3642 $new_date->set_minute($original_date->minute());
3643 if ($new_date >= DateTime->now) {
3644 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3647 $bd = cleanse_ISO8601($new_date->datetime());
3650 $self->backdate($bd);
3655 sub check_checkin_copy_status {
3657 my $copy = $self->copy;
3659 my $status = $U->copy_status($copy->status)->id;
3662 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3663 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3664 $status == OILS_COPY_STATUS_IN_PROCESS ||
3665 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3666 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3667 $status == OILS_COPY_STATUS_CATALOGING ||
3668 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3669 $status == OILS_COPY_STATUS_RESHELVING );
3671 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3672 if( $status == OILS_COPY_STATUS_LOST );
3674 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3675 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3677 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3678 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3680 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3681 if( $status == OILS_COPY_STATUS_MISSING );
3683 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3688 # --------------------------------------------------------------------------
3689 # On checkin, we need to return as many relevant objects as we can
3690 # --------------------------------------------------------------------------
3691 sub checkin_flesh_events {
3694 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3695 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3696 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3699 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3702 if($self->hold and !$self->hold->cancel_time) {
3703 $hold = $self->hold;
3704 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3708 # update our copy of the circ object and
3709 # flesh the billing summary data
3711 $self->editor->retrieve_action_circulation([
3715 circ => ['billable_transaction'],
3724 # flesh some patron fields before returning
3726 $self->editor->retrieve_actor_user([
3731 au => ['card', 'billing_address', 'mailing_address']
3738 for my $evt (@{$self->events}) {
3741 $payload->{copy} = $U->unflesh_copy($self->copy);
3742 $payload->{volume} = $self->volume;
3743 $payload->{record} = $record,
3744 $payload->{circ} = $self->circ;
3745 $payload->{transit} = $self->transit;
3746 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3747 $payload->{hold} = $hold;
3748 $payload->{patron} = $self->patron;
3749 $payload->{reservation} = $self->reservation
3750 unless (not $self->reservation or $self->reservation->cancel_time);
3752 $evt->{payload} = $payload;
3757 my( $self, $msg ) = @_;
3758 my $bc = ($self->copy) ? $self->copy->barcode :
3761 my $usr = ($self->patron) ? $self->patron->id : "";
3762 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3763 ", recipient=$usr, copy=$bc");
3769 $self->log_me("do_renew()");
3771 # Make sure there is an open circ to renew that is not
3772 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3773 my $usrid = $self->patron->id if $self->patron;
3774 my $circ = $self->editor->search_action_circulation({
3775 target_copy => $self->copy->id,
3776 xact_finish => undef,
3777 checkin_time => undef,
3778 ($usrid ? (usr => $usrid) : ()),
3780 {stop_fines => undef},
3781 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3785 return $self->bail_on_events($self->editor->event) unless $circ;
3787 # A user is not allowed to renew another user's items without permission
3788 unless( $circ->usr eq $self->editor->requestor->id ) {
3789 return $self->bail_on_events($self->editor->events)
3790 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3793 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3794 if $circ->renewal_remaining < 1;
3796 # -----------------------------------------------------------------
3798 $self->parent_circ($circ->id);
3799 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3802 # Opac renewal - re-use circ library from original circ (unless told not to)
3803 if($self->opac_renewal) {
3804 unless(defined($opac_renewal_use_circ_lib)) {
3805 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3806 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3807 $opac_renewal_use_circ_lib = 1;
3810 $opac_renewal_use_circ_lib = 0;
3813 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3816 # Desk renewal - re-use circ library from original circ (unless told not to)
3817 if($self->desk_renewal) {
3818 unless(defined($desk_renewal_use_circ_lib)) {
3819 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3820 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3821 $desk_renewal_use_circ_lib = 1;
3824 $desk_renewal_use_circ_lib = 0;
3827 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3830 # Run the fine generator against the old circ
3831 $self->generate_fines_start;
3833 $self->run_renew_permit;
3836 $self->do_checkin();
3837 return if $self->bail_out;
3839 unless( $self->permit_override ) {
3841 return if $self->bail_out;
3842 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3843 $self->remove_event('ITEM_NOT_CATALOGED');
3846 $self->override_events;
3847 return if $self->bail_out;
3850 $self->do_checkout();
3855 my( $self, $evt ) = @_;
3856 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3857 $logger->debug("circulator: removing event from list: $evt");
3858 my @events = @{$self->events};
3859 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3864 my( $self, $evt ) = @_;
3865 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3866 return grep { $_->{textcode} eq $evt } @{$self->events};
3871 sub run_renew_permit {
3874 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3875 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3876 $self->editor, $self->copy, $self->editor->requestor, 1
3878 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3881 if(!$self->legacy_script_support) {
3882 my $results = $self->run_indb_circ_test;
3883 $self->push_events($self->matrix_test_result_events)
3884 unless $self->circ_test_success;
3887 my $runner = $self->script_runner;
3889 $runner->load($self->circ_permit_renew);
3890 my $result = $runner->run or
3891 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3892 if ($result->{"events"}) {
3894 map { new OpenILS::Event($_) } @{$result->{"events"}}
3897 "circulator: circ_permit_renew for user " .
3898 $self->patron->id . " returned " .
3899 scalar(@{$result->{"events"}}) . " event(s)"
3903 $self->mk_script_runner;
3906 $logger->debug("circulator: re-creating script runner to be safe");
3910 # XXX: The primary mechanism for storing circ history is now handled
3911 # by tracking real circulation objects instead of bibs in a bucket.
3912 # However, this code is disabled by default and could be useful
3913 # some day, so may as well leave it for now.
3914 sub append_reading_list {
3918 $self->is_checkout and
3924 # verify history is globally enabled and uses the bucket mechanism
3925 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3926 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3928 return undef unless $htype and $htype eq 'bucket';
3930 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3932 # verify the patron wants to retain the hisory
3933 my $setting = $e->search_actor_user_setting(
3934 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3936 unless($setting and $setting->value) {
3941 my $bkt = $e->search_container_copy_bucket(
3942 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3947 # find the next item position
3948 my $last_item = $e->search_container_copy_bucket_item(
3949 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3950 $pos = $last_item->pos + 1 if $last_item;
3953 # create the history bucket if necessary
3954 $bkt = Fieldmapper::container::copy_bucket->new;
3955 $bkt->owner($self->patron->id);
3957 $bkt->btype('circ_history');
3959 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3962 my $item = Fieldmapper::container::copy_bucket_item->new;
3964 $item->bucket($bkt->id);
3965 $item->target_copy($self->copy->id);
3968 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3975 sub make_trigger_events {
3977 return unless $self->circ;
3978 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3979 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3980 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3985 sub checkin_handle_lost_or_lo_now_found {
3986 my ($self, $bill_type, $is_longoverdue) = @_;
3988 # ------------------------------------------------------------------
3989 # remove charge from patron's account if lost item is returned
3990 # ------------------------------------------------------------------
3992 my $bills = $self->editor->search_money_billing(
3994 xact => $self->circ->id,
3999 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4001 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
4002 for my $bill (@$bills) {
4003 if( !$U->is_true($bill->voided) ) {
4004 $logger->info("$tag item returned - voiding bill ".$bill->id);
4006 $bill->void_time('now');
4007 $bill->voider($self->editor->requestor->id);
4008 my $note = ($bill->note) ? $bill->note . "\n" : '';
4009 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
4011 $self->bail_on_events($self->editor->event)
4012 unless $self->editor->update_money_billing($bill);
4017 sub checkin_handle_lost_or_lo_now_found_restore_od {
4019 my $circ_lib = shift;
4020 my $is_longoverdue = shift;
4021 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4023 # ------------------------------------------------------------------
4024 # restore those overdue charges voided when item was set to lost
4025 # ------------------------------------------------------------------
4027 my $ods = $self->editor->search_money_billing(
4029 xact => $self->circ->id,
4034 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4035 for my $bill (@$ods) {
4036 if( $U->is_true($bill->voided) ) {
4037 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4039 $bill->clear_void_time;
4040 $bill->voider($self->editor->requestor->id);
4041 my $note = ($bill->note) ? $bill->note . "\n" : '';
4042 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4044 $self->bail_on_events($self->editor->event)
4045 unless $self->editor->update_money_billing($bill);
4050 # ------------------------------------------------------------------
4051 # Lost-then-found item checked in. This sub generates new overdue
4052 # fines, beyond the point of any existing and possibly voided
4053 # overdue fines, up to the point of final checkin time (or max fine
4055 # ------------------------------------------------------------------
4056 sub generate_lost_overdue_fines {
4058 my $circ = $self->circ;
4059 my $e = $self->editor;
4061 # Re-open the transaction so the fine generator can see it
4062 if($circ->xact_finish or $circ->stop_fines) {
4064 $circ->clear_xact_finish;
4065 $circ->clear_stop_fines;
4066 $circ->clear_stop_fines_time;
4067 $e->update_action_circulation($circ) or return $e->die_event;
4071 $e->xact_begin; # generate_fines expects an in-xact editor
4072 $self->generate_fines;
4073 $circ = $self->circ; # generate fines re-fetches the circ
4077 # Re-close the transaction if no money is owed
4078 my ($obt) = $U->fetch_mbts($circ->id, $e);
4079 if ($obt and $obt->balance_owed == 0) {
4080 $circ->xact_finish('now');
4084 # Set stop fines if the fine generator didn't have to
4085 unless($circ->stop_fines) {
4086 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
4087 $circ->stop_fines_time('now');
4091 # update the event data sent to the caller within the transaction
4092 $self->checkin_flesh_events;
4095 $e->update_action_circulation($circ) or return $e->die_event;