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'], 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
552 my $type = ref($self) or die "$self is not an object";
554 my $name = $AUTOLOAD;
557 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
558 $logger->error("circulator: $type: invalid autoload field: $name");
559 die "$type: invalid autoload field: $name\n"
564 *{"${type}::${name}"} = sub {
567 $s->{$name} = $v if defined $v;
571 return $self->$name($data);
576 my( $class, $auth, %args ) = @_;
577 $class = ref($class) || $class;
578 my $self = bless( {}, $class );
581 $self->editor(new_editor(xact => 1, authtoken => $auth));
583 unless( $self->editor->checkauth ) {
584 $self->bail_on_events($self->editor->event);
588 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
590 $self->$_($args{$_}) for keys %args;
593 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
595 # if this is a renewal, default to desk_renewal
596 $self->desk_renewal(1) unless
597 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
599 $self->capture('') unless $self->capture;
601 unless(%user_groups) {
602 my $gps = $self->editor->retrieve_all_permission_grp_tree;
603 %user_groups = map { $_->id => $_ } @$gps;
610 # --------------------------------------------------------------------------
611 # True if we should discontinue processing
612 # --------------------------------------------------------------------------
614 my( $self, $bool ) = @_;
615 if( defined $bool ) {
616 $logger->info("circulator: BAILING OUT") if $bool;
617 $self->{bail_out} = $bool;
619 return $self->{bail_out};
624 my( $self, @evts ) = @_;
627 $e->{payload} = $self->copy if
628 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
630 $logger->info("circulator: pushing event ".$e->{textcode});
631 push( @{$self->events}, $e ) unless
632 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
638 return '' if $self->skip_permit_key;
639 my $key = md5_hex( time() . rand() . "$$" );
640 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
641 return $self->permit_key($key);
644 sub check_permit_key {
646 return 1 if $self->skip_permit_key;
647 my $key = $self->permit_key;
648 return 0 unless $key;
649 my $k = "oils_permit_key_$key";
650 my $one = $self->cache_handle->get_cache($k);
651 $self->cache_handle->delete_cache($k);
652 return ($one) ? 1 : 0;
655 sub seems_like_reservation {
658 # Some words about the following method:
659 # 1) It requires the VIEW_USER permission, but that's not an
660 # issue, right, since all staff should have that?
661 # 2) It returns only one reservation at a time, even if an item can be
662 # and is currently overbooked. Hmmm....
663 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
664 my $result = $booking_ses->request(
665 "open-ils.booking.reservations.by_returnable_resource_barcode",
666 $self->editor->authtoken,
669 $booking_ses->disconnect;
671 return $self->bail_on_events($result) if defined $U->event_code($result);
674 $self->reservation(shift @$result);
682 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
683 sub save_trimmed_copy {
684 my ($self, $copy) = @_;
687 $self->volume($copy->call_number);
688 $self->title($self->volume->record);
689 $self->copy->call_number($self->volume->id);
690 $self->volume->record($self->title->id);
691 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
692 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
693 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
694 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
700 my $e = $self->editor;
702 # --------------------------------------------------------------------------
703 # Grab the fleshed copy
704 # --------------------------------------------------------------------------
705 unless($self->is_noncat) {
708 $copy = $e->retrieve_asset_copy(
709 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
711 } elsif( $self->copy_barcode ) {
713 $copy = $e->search_asset_copy(
714 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
715 } elsif( $self->reservation ) {
716 my $res = $e->json_query(
718 "select" => {"acp" => ["id"]},
723 "field" => "barcode",
727 "field" => "current_resource"
735 "id" => (ref $self->reservation) ?
736 $self->reservation->id : $self->reservation
741 if (ref $res eq "ARRAY" and scalar @$res) {
742 $logger->info("circulator: mapped reservation " .
743 $self->reservation . " to copy " . $res->[0]->{"id"});
744 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
749 $self->save_trimmed_copy($copy);
751 # We can't renew if there is no copy
752 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
753 if $self->is_renewal;
758 # --------------------------------------------------------------------------
760 # --------------------------------------------------------------------------
764 flesh_fields => {au => [ qw/ card / ]}
767 if( $self->patron_id ) {
768 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
769 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
771 } elsif( $self->patron_barcode ) {
773 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
774 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
775 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
777 $patron = $e->retrieve_actor_user($card->usr)
778 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
780 # Use the card we looked up, not the patron's primary, for card active checks
781 $patron->card($card);
784 if( my $copy = $self->copy ) {
787 $flesh->{flesh_fields}->{circ} = ['usr'];
789 my $circ = $e->search_action_circulation([
790 {target_copy => $copy->id, checkin_time => undef}, $flesh
794 $patron = $circ->usr;
795 $circ->usr($patron->id); # de-flesh for consistency
801 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
802 unless $self->patron($patron) or $self->is_checkin;
804 unless($self->is_checkin) {
806 # Check for inactivity and patron reg. expiration
808 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
809 unless $U->is_true($patron->active);
811 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
812 unless $U->is_true($patron->card->active);
814 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
815 cleanse_ISO8601($patron->expire_date));
817 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
818 if( CORE::time > $expire->epoch ) ;
822 # --------------------------------------------------------------------------
823 # This builds the script runner environment and fetches most of the
825 # --------------------------------------------------------------------------
826 sub mk_script_runner {
832 qw/copy copy_barcode copy_id patron
833 patron_id patron_barcode volume title editor/;
835 # Translate our objects into the ScriptBuilder args hash
836 $$args{$_} = $self->$_() for @fields;
838 $args->{ignore_user_status} = 1 if $self->is_checkin;
839 $$args{fetch_patron_by_circ_copy} = 1;
840 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
842 if( my $pco = $self->pending_checkouts ) {
843 $logger->info("circulator: we were given a pending checkouts number of $pco");
844 $$args{patronItemsOut} = $pco;
847 # This fetches most of the objects we need
848 $self->script_runner(
849 OpenILS::Application::Circ::ScriptBuilder->build($args));
851 # Now we translate the ScriptBuilder objects back into self
852 $self->$_($$args{$_}) for @fields;
854 my @evts = @{$args->{_events}} if $args->{_events};
856 $logger->debug("circulator: script builder returned events: @evts") if @evts;
860 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
861 if(!$self->is_noncat and
863 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
867 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
868 return $self->bail_on_events(@e);
873 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
874 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
875 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
876 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
880 # We can't renew if there is no copy
881 return $self->bail_on_events(@evts) if
882 $self->is_renewal and !$self->copy;
884 # Set some circ-specific flags in the script environment
885 my $evt = "environment";
886 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
888 if( $self->is_noncat ) {
889 $self->script_runner->insert("$evt.isNonCat", 1);
890 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
893 if( $self->is_precat ) {
894 $self->script_runner->insert("environment.isPrecat", 1, 1);
897 $self->script_runner->add_path( $_ ) for @$script_libs;
902 # --------------------------------------------------------------------------
903 # Does the circ permit work
904 # --------------------------------------------------------------------------
908 $self->log_me("do_permit()");
910 unless( $self->editor->requestor->id == $self->patron->id ) {
911 return $self->bail_on_events($self->editor->event)
912 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
915 $self->check_captured_holds();
916 $self->do_copy_checks();
917 return if $self->bail_out;
918 $self->run_patron_permit_scripts();
919 $self->run_copy_permit_scripts()
920 unless $self->is_precat or $self->is_noncat;
921 $self->check_item_deposit_events();
922 $self->override_events();
923 return if $self->bail_out;
925 if($self->is_precat and not $self->request_precat) {
928 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
929 return $self->bail_out(1) unless $self->is_renewal;
933 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
936 sub check_item_deposit_events {
938 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
939 if $self->is_deposit and not $self->is_deposit_exempt;
940 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
941 if $self->is_rental and not $self->is_rental_exempt;
944 # returns true if the user is not required to pay deposits
945 sub is_deposit_exempt {
947 my $pid = (ref $self->patron->profile) ?
948 $self->patron->profile->id : $self->patron->profile;
949 my $groups = $U->ou_ancestor_setting_value(
950 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
951 for my $grp (@$groups) {
952 return 1 if $self->is_group_descendant($grp, $pid);
957 # returns true if the user is not required to pay rental fees
958 sub is_rental_exempt {
960 my $pid = (ref $self->patron->profile) ?
961 $self->patron->profile->id : $self->patron->profile;
962 my $groups = $U->ou_ancestor_setting_value(
963 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
964 for my $grp (@$groups) {
965 return 1 if $self->is_group_descendant($grp, $pid);
970 sub is_group_descendant {
971 my($self, $p_id, $c_id) = @_;
972 return 0 unless defined $p_id and defined $c_id;
973 return 1 if $c_id == $p_id;
974 while(my $grp = $user_groups{$c_id}) {
975 $c_id = $grp->parent;
976 return 0 unless defined $c_id;
977 return 1 if $c_id == $p_id;
982 sub check_captured_holds {
984 my $copy = $self->copy;
985 my $patron = $self->patron;
987 return undef unless $copy;
989 my $s = $U->copy_status($copy->status)->id;
990 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
991 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
993 # Item is on the holds shelf, make sure it's going to the right person
994 my $hold = $self->editor->search_action_hold_request(
997 current_copy => $copy->id ,
998 capture_time => { '!=' => undef },
999 cancel_time => undef,
1000 fulfillment_time => undef
1006 if ($hold and $hold->usr == $patron->id) {
1007 $self->checkout_is_for_hold(1);
1011 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1013 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1017 sub do_copy_checks {
1019 my $copy = $self->copy;
1020 return unless $copy;
1022 my $stat = $U->copy_status($copy->status)->id;
1024 # We cannot check out a copy if it is in-transit
1025 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1026 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1029 $self->handle_claims_returned();
1030 return if $self->bail_out;
1032 # no claims returned circ was found, check if there is any open circ
1033 unless( $self->is_renewal ) {
1035 my $circs = $self->editor->search_action_circulation(
1036 { target_copy => $copy->id, checkin_time => undef }
1039 if(my $old_circ = $circs->[0]) { # an open circ was found
1041 my $payload = {copy => $copy};
1043 if($old_circ->usr == $self->patron->id) {
1045 $payload->{old_circ} = $old_circ;
1047 # If there is an open circulation on the checkout item and an auto-renew
1048 # interval is defined, inform the caller that they should go
1049 # ahead and renew the item instead of warning about open circulations.
1051 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1053 'circ.checkout_auto_renew_age',
1057 if($auto_renew_intvl) {
1058 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1059 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1061 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1062 $payload->{auto_renew} = 1;
1067 return $self->bail_on_events(
1068 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1074 my $LEGACY_CIRC_EVENT_MAP = {
1075 'no_item' => 'ITEM_NOT_CATALOGED',
1076 'actor.usr.barred' => 'PATRON_BARRED',
1077 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1078 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1079 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1080 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1081 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1082 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1083 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1084 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1088 # ---------------------------------------------------------------------
1089 # This pushes any patron-related events into the list but does not
1090 # set bail_out for any events
1091 # ---------------------------------------------------------------------
1092 sub run_patron_permit_scripts {
1094 my $runner = $self->script_runner;
1095 my $patronid = $self->patron->id;
1099 if(!$self->legacy_script_support) {
1101 my $results = $self->run_indb_circ_test;
1102 unless($self->circ_test_success) {
1103 my @trimmed_results;
1105 if ($self->is_noncat) {
1106 # no_item result is OK during noncat checkout
1107 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1111 if ($self->checkout_is_for_hold) {
1112 # if this checkout will fulfill a hold, ignore CIRC blocks
1113 # and rely instead on the (later-checked) FULFILL block
1115 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1116 my $fblock_pens = $self->editor->search_config_standing_penalty(
1117 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1119 for my $res (@$results) {
1120 my $name = $res->{fail_part} || '';
1121 next if grep {$_->name eq $name} @$fblock_pens;
1122 push(@trimmed_results, $res);
1126 # not for hold or noncat
1127 @trimmed_results = @$results;
1131 # update the final set of test results
1132 $self->matrix_test_result(\@trimmed_results);
1134 push @allevents, $self->matrix_test_result_events;
1139 # ---------------------------------------------------------------------
1140 # # Now run the patron permit script
1141 # ---------------------------------------------------------------------
1142 $runner->load($self->circ_permit_patron);
1143 my $result = $runner->run or
1144 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1146 my $patron_events = $result->{events};
1148 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1149 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1150 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1151 $penalties = $penalties->{fatal_penalties};
1153 for my $pen (@$penalties) {
1154 # CIRC blocks are ignored if this is a FULFILL scenario
1155 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1156 my $event = OpenILS::Event->new($pen->name);
1157 $event->{desc} = $pen->label;
1158 push(@allevents, $event);
1161 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1165 $_->{payload} = $self->copy if
1166 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1169 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1171 $self->push_events(@allevents);
1174 sub matrix_test_result_codes {
1176 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1179 sub matrix_test_result_events {
1182 my $event = new OpenILS::Event(
1183 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1185 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1187 } (@{$self->matrix_test_result});
1190 sub run_indb_circ_test {
1192 return $self->matrix_test_result if $self->matrix_test_result;
1194 my $dbfunc = ($self->is_renewal) ?
1195 'action.item_user_renew_test' : 'action.item_user_circ_test';
1197 if( $self->is_precat && $self->request_precat) {
1198 $self->make_precat_copy;
1199 return if $self->bail_out;
1202 my $results = $self->editor->json_query(
1206 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1212 $self->circ_test_success($U->is_true($results->[0]->{success}));
1214 if(my $mp = $results->[0]->{matchpoint}) {
1215 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1216 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1217 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1218 if(defined($results->[0]->{renewals})) {
1219 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1221 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1222 if(defined($results->[0]->{grace_period})) {
1223 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1225 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1226 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1227 # Grab the *last* response for limit_groups, where it is more likely to be filled
1228 $self->limit_groups($results->[-1]->{limit_groups});
1231 return $self->matrix_test_result($results);
1234 # ---------------------------------------------------------------------
1235 # given a use and copy, this will calculate the circulation policy
1236 # parameters. Only works with in-db circ.
1237 # ---------------------------------------------------------------------
1241 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1243 $self->run_indb_circ_test;
1246 circ_test_success => $self->circ_test_success,
1247 failure_events => [],
1248 failure_codes => [],
1249 matchpoint => $self->circ_matrix_matchpoint
1252 unless($self->circ_test_success) {
1253 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1254 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1257 if($self->circ_matrix_matchpoint) {
1258 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1259 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1260 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1261 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1263 my $policy = $self->get_circ_policy(
1264 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1266 $$results{$_} = $$policy{$_} for keys %$policy;
1272 # ---------------------------------------------------------------------
1273 # Loads the circ policy info for duration, recurring fine, and max
1274 # fine based on the current copy
1275 # ---------------------------------------------------------------------
1276 sub get_circ_policy {
1277 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1280 duration_rule => $duration_rule->name,
1281 recurring_fine_rule => $recurring_fine_rule->name,
1282 max_fine_rule => $max_fine_rule->name,
1283 max_fine => $self->get_max_fine_amount($max_fine_rule),
1284 fine_interval => $recurring_fine_rule->recurrence_interval,
1285 renewal_remaining => $duration_rule->max_renewals,
1286 grace_period => $recurring_fine_rule->grace_period
1289 if($hard_due_date) {
1290 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1291 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1294 $policy->{duration_date_ceiling} = undef;
1295 $policy->{duration_date_ceiling_force} = undef;
1298 $policy->{duration} = $duration_rule->shrt
1299 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1300 $policy->{duration} = $duration_rule->normal
1301 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1302 $policy->{duration} = $duration_rule->extended
1303 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1305 $policy->{recurring_fine} = $recurring_fine_rule->low
1306 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1307 $policy->{recurring_fine} = $recurring_fine_rule->normal
1308 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1309 $policy->{recurring_fine} = $recurring_fine_rule->high
1310 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1315 sub get_max_fine_amount {
1317 my $max_fine_rule = shift;
1318 my $max_amount = $max_fine_rule->amount;
1320 # if is_percent is true then the max->amount is
1321 # use as a percentage of the copy price
1322 if ($U->is_true($max_fine_rule->is_percent)) {
1323 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1324 $max_amount = $price * $max_fine_rule->amount / 100;
1326 $U->ou_ancestor_setting_value(
1328 'circ.max_fine.cap_at_price',
1332 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1333 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1341 sub run_copy_permit_scripts {
1343 my $copy = $self->copy || return;
1344 my $runner = $self->script_runner;
1348 if(!$self->legacy_script_support) {
1349 my $results = $self->run_indb_circ_test;
1350 push @allevents, $self->matrix_test_result_events
1351 unless $self->circ_test_success;
1354 # ---------------------------------------------------------------------
1355 # Capture all of the copy permit events
1356 # ---------------------------------------------------------------------
1357 $runner->load($self->circ_permit_copy);
1358 my $result = $runner->run or
1359 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1360 my $copy_events = $result->{events};
1362 # ---------------------------------------------------------------------
1363 # Now collect all of the events together
1364 # ---------------------------------------------------------------------
1365 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1368 # See if this copy has an alert message
1369 my $ae = $self->check_copy_alert();
1370 push( @allevents, $ae ) if $ae;
1372 # uniquify the events
1373 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1374 @allevents = values %hash;
1376 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1378 $self->push_events(@allevents);
1382 sub check_copy_alert {
1384 return undef if $self->is_renewal;
1385 return OpenILS::Event->new(
1386 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1387 if $self->copy and $self->copy->alert_message;
1393 # --------------------------------------------------------------------------
1394 # If the call is overriding and has permissions to override every collected
1395 # event, the are cleared. Any event that the caller does not have
1396 # permission to override, will be left in the event list and bail_out will
1398 # XXX We need code in here to cancel any holds/transits on copies
1399 # that are being force-checked out
1400 # --------------------------------------------------------------------------
1401 sub override_events {
1403 my @events = @{$self->events};
1404 return unless @events;
1405 my $oargs = $self->override_args;
1407 if(!$self->override) {
1408 return $self->bail_out(1)
1409 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1414 for my $e (@events) {
1415 my $tc = $e->{textcode};
1416 next if $tc eq 'SUCCESS';
1417 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1418 my $ov = "$tc.override";
1419 $logger->info("circulator: attempting to override event: $ov");
1421 return $self->bail_on_events($self->editor->event)
1422 unless( $self->editor->allowed($ov) );
1424 return $self->bail_out(1);
1430 # --------------------------------------------------------------------------
1431 # If there is an open claimsreturn circ on the requested copy, close the
1432 # circ if overriding, otherwise bail out
1433 # --------------------------------------------------------------------------
1434 sub handle_claims_returned {
1436 my $copy = $self->copy;
1438 my $CR = $self->editor->search_action_circulation(
1440 target_copy => $copy->id,
1441 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1442 checkin_time => undef,
1446 return unless ($CR = $CR->[0]);
1450 # - If the caller has set the override flag, we will check the item in
1451 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1453 $CR->checkin_time('now');
1454 $CR->checkin_scan_time('now');
1455 $CR->checkin_lib($self->circ_lib);
1456 $CR->checkin_workstation($self->editor->requestor->wsid);
1457 $CR->checkin_staff($self->editor->requestor->id);
1459 $evt = $self->editor->event
1460 unless $self->editor->update_action_circulation($CR);
1463 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1466 $self->bail_on_events($evt) if $evt;
1471 # --------------------------------------------------------------------------
1472 # This performs the checkout
1473 # --------------------------------------------------------------------------
1477 $self->log_me("do_checkout()");
1479 # make sure perms are good if this isn't a renewal
1480 unless( $self->is_renewal ) {
1481 return $self->bail_on_events($self->editor->event)
1482 unless( $self->editor->allowed('COPY_CHECKOUT') );
1485 # verify the permit key
1486 unless( $self->check_permit_key ) {
1487 if( $self->permit_override ) {
1488 return $self->bail_on_events($self->editor->event)
1489 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1491 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1495 # if this is a non-cataloged circ, build the circ and finish
1496 if( $self->is_noncat ) {
1497 $self->checkout_noncat;
1499 OpenILS::Event->new('SUCCESS',
1500 payload => { noncat_circ => $self->circ }));
1504 if( $self->is_precat ) {
1505 $self->make_precat_copy;
1506 return if $self->bail_out;
1508 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1509 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1512 $self->do_copy_checks;
1513 return if $self->bail_out;
1515 $self->run_checkout_scripts();
1516 return if $self->bail_out;
1518 $self->build_checkout_circ_object();
1519 return if $self->bail_out;
1521 my $modify_to_start = $self->booking_adjusted_due_date();
1522 return if $self->bail_out;
1524 $self->apply_modified_due_date($modify_to_start);
1525 return if $self->bail_out;
1527 return $self->bail_on_events($self->editor->event)
1528 unless $self->editor->create_action_circulation($self->circ);
1530 # refresh the circ to force local time zone for now
1531 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1533 if($self->limit_groups) {
1534 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1537 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1539 return if $self->bail_out;
1541 $self->apply_deposit_fee();
1542 return if $self->bail_out;
1544 $self->handle_checkout_holds();
1545 return if $self->bail_out;
1547 # ------------------------------------------------------------------------------
1548 # Update the patron penalty info in the DB. Run it for permit-overrides
1549 # since the penalties are not updated during the permit phase
1550 # ------------------------------------------------------------------------------
1551 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1553 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1556 if($self->is_renewal) {
1557 # flesh the billing summary for the checked-in circ
1558 $pcirc = $self->editor->retrieve_action_circulation([
1560 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1565 OpenILS::Event->new('SUCCESS',
1567 copy => $U->unflesh_copy($self->copy),
1568 volume => $self->volume,
1569 circ => $self->circ,
1571 holds_fulfilled => $self->fulfilled_holds,
1572 deposit_billing => $self->deposit_billing,
1573 rental_billing => $self->rental_billing,
1574 parent_circ => $pcirc,
1575 patron => ($self->return_patron) ? $self->patron : undef,
1576 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1582 sub apply_deposit_fee {
1584 my $copy = $self->copy;
1586 ($self->is_deposit and not $self->is_deposit_exempt) or
1587 ($self->is_rental and not $self->is_rental_exempt);
1589 return if $self->is_deposit and $self->skip_deposit_fee;
1590 return if $self->is_rental and $self->skip_rental_fee;
1592 my $bill = Fieldmapper::money::billing->new;
1593 my $amount = $copy->deposit_amount;
1597 if($self->is_deposit) {
1598 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1600 $self->deposit_billing($bill);
1602 $billing_type = OILS_BILLING_TYPE_RENTAL;
1604 $self->rental_billing($bill);
1607 $bill->xact($self->circ->id);
1608 $bill->amount($amount);
1609 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1610 $bill->billing_type($billing_type);
1611 $bill->btype($btype);
1612 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1614 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1619 my $copy = $self->copy;
1621 my $stat = $copy->status if ref $copy->status;
1622 my $loc = $copy->location if ref $copy->location;
1623 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1625 $copy->status($stat->id) if $stat;
1626 $copy->location($loc->id) if $loc;
1627 $copy->circ_lib($circ_lib->id) if $circ_lib;
1628 $copy->editor($self->editor->requestor->id);
1629 $copy->edit_date('now');
1630 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1632 return $self->bail_on_events($self->editor->event)
1633 unless $self->editor->update_asset_copy($self->copy);
1635 $copy->status($U->copy_status($copy->status));
1636 $copy->location($loc) if $loc;
1637 $copy->circ_lib($circ_lib) if $circ_lib;
1640 sub update_reservation {
1642 my $reservation = $self->reservation;
1644 my $usr = $reservation->usr;
1645 my $target_rt = $reservation->target_resource_type;
1646 my $target_r = $reservation->target_resource;
1647 my $current_r = $reservation->current_resource;
1649 $reservation->usr($usr->id) if ref $usr;
1650 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1651 $reservation->target_resource($target_r->id) if ref $target_r;
1652 $reservation->current_resource($current_r->id) if ref $current_r;
1654 return $self->bail_on_events($self->editor->event)
1655 unless $self->editor->update_booking_reservation($self->reservation);
1658 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1659 $self->reservation($reservation);
1663 sub bail_on_events {
1664 my( $self, @evts ) = @_;
1665 $self->push_events(@evts);
1669 # ------------------------------------------------------------------------------
1670 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1671 # affects copies that will fulfill holds and CIRC affects all other copies.
1672 # If blocks exists, bail, push Events onto the event pile, and return true.
1673 # ------------------------------------------------------------------------------
1674 sub check_hold_fulfill_blocks {
1677 # See if the user has any penalties applied that prevent hold fulfillment
1678 my $pens = $self->editor->json_query({
1679 select => {csp => ['name', 'label']},
1680 from => {ausp => {csp => {}}},
1683 usr => $self->patron->id,
1684 org_unit => $U->get_org_full_path($self->circ_lib),
1686 {stop_date => undef},
1687 {stop_date => {'>' => 'now'}}
1690 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1694 return 0 unless @$pens;
1696 for my $pen (@$pens) {
1697 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1698 my $event = OpenILS::Event->new($pen->{name});
1699 $event->{desc} = $pen->{label};
1700 $self->push_events($event);
1703 $self->override_events;
1704 return $self->bail_out;
1708 # ------------------------------------------------------------------------------
1709 # When an item is checked out, see if we can fulfill a hold for this patron
1710 # ------------------------------------------------------------------------------
1711 sub handle_checkout_holds {
1713 my $copy = $self->copy;
1714 my $patron = $self->patron;
1716 my $e = $self->editor;
1717 $self->fulfilled_holds([]);
1719 # pre/non-cats can't fulfill a hold
1720 return if $self->is_precat or $self->is_noncat;
1722 my $hold = $e->search_action_hold_request({
1723 current_copy => $copy->id ,
1724 cancel_time => undef,
1725 fulfillment_time => undef,
1727 {expire_time => undef},
1728 {expire_time => {'>' => 'now'}}
1732 if($hold and $hold->usr != $patron->id) {
1733 # reset the hold since the copy is now checked out
1735 $logger->info("circulator: un-targeting hold ".$hold->id.
1736 " because copy ".$copy->id." is getting checked out");
1738 $hold->clear_prev_check_time;
1739 $hold->clear_current_copy;
1740 $hold->clear_capture_time;
1741 $hold->clear_shelf_time;
1742 $hold->clear_shelf_expire_time;
1743 $hold->clear_current_shelf_lib;
1745 return $self->bail_on_event($e->event)
1746 unless $e->update_action_hold_request($hold);
1752 $hold = $self->find_related_user_hold($copy, $patron) or return;
1753 $logger->info("circulator: found related hold to fulfill in checkout");
1756 return if $self->check_hold_fulfill_blocks;
1758 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1760 # if the hold was never officially captured, capture it.
1761 $hold->current_copy($copy->id);
1762 $hold->capture_time('now') unless $hold->capture_time;
1763 $hold->fulfillment_time('now');
1764 $hold->fulfillment_staff($e->requestor->id);
1765 $hold->fulfillment_lib($self->circ_lib);
1767 return $self->bail_on_events($e->event)
1768 unless $e->update_action_hold_request($hold);
1770 $holdcode->delete_hold_copy_maps($e, $hold->id);
1771 return $self->fulfilled_holds([$hold->id]);
1775 # ------------------------------------------------------------------------------
1776 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1777 # the patron directly targets the checked out item, see if there is another hold
1778 # for the patron that could be fulfilled by the checked out item. Fulfill the
1779 # oldest hold and only fulfill 1 of them.
1781 # For "another hold":
1783 # First, check for one that the copy matches via hold_copy_map, ensuring that
1784 # *any* hold type that this copy could fill may end up filled.
1786 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1787 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1788 # that are non-requestable to count as capturing those hold types.
1789 # ------------------------------------------------------------------------------
1790 sub find_related_user_hold {
1791 my($self, $copy, $patron) = @_;
1792 my $e = $self->editor;
1794 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1796 return undef unless $U->ou_ancestor_setting_value(
1797 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1799 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1801 select => {ahr => ['id']},
1810 fkey => 'current_copy',
1811 type => 'left' # there may be no current_copy
1818 fulfillment_time => undef,
1819 cancel_time => undef,
1821 {expire_time => undef},
1822 {expire_time => {'>' => 'now'}}
1826 target_copy => $self->copy->id
1830 {id => undef}, # left-join copy may be nonexistent
1831 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1835 order_by => {ahr => {request_time => {direction => 'asc'}}},
1839 my $hold_info = $e->json_query($args)->[0];
1840 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1841 return undef if $U->ou_ancestor_setting_value(
1842 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1844 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1846 select => {ahr => ['id']},
1851 fkey => 'current_copy',
1852 type => 'left' # there may be no current_copy
1859 fulfillment_time => undef,
1860 cancel_time => undef,
1862 {expire_time => undef},
1863 {expire_time => {'>' => 'now'}}
1870 target => $self->volume->id
1876 target => $self->title->id
1882 {id => undef}, # left-join copy may be nonexistent
1883 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1887 order_by => {ahr => {request_time => {direction => 'asc'}}},
1891 $hold_info = $e->json_query($args)->[0];
1892 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1897 sub run_checkout_scripts {
1902 my $runner = $self->script_runner;
1911 my $hard_due_date_name;
1913 if(!$self->legacy_script_support) {
1914 $self->run_indb_circ_test();
1915 $duration = $self->circ_matrix_matchpoint->duration_rule;
1916 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1917 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1918 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1922 $runner->load($self->circ_duration);
1924 my $result = $runner->run or
1925 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1927 $duration_name = $result->{durationRule};
1928 $recurring_name = $result->{recurringFinesRule};
1929 $max_fine_name = $result->{maxFine};
1930 $hard_due_date_name = $result->{hardDueDate};
1933 $duration_name = $duration->name if $duration;
1934 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1937 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1938 return $self->bail_on_events($evt) if ($evt && !$nobail);
1940 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1941 return $self->bail_on_events($evt) if ($evt && !$nobail);
1943 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1944 return $self->bail_on_events($evt) if ($evt && !$nobail);
1946 if($hard_due_date_name) {
1947 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1948 return $self->bail_on_events($evt) if ($evt && !$nobail);
1954 # The item circulates with an unlimited duration
1958 $hard_due_date = undef;
1961 $self->duration_rule($duration);
1962 $self->recurring_fines_rule($recurring);
1963 $self->max_fine_rule($max_fine);
1964 $self->hard_due_date($hard_due_date);
1968 sub build_checkout_circ_object {
1971 my $circ = Fieldmapper::action::circulation->new;
1972 my $duration = $self->duration_rule;
1973 my $max = $self->max_fine_rule;
1974 my $recurring = $self->recurring_fines_rule;
1975 my $hard_due_date = $self->hard_due_date;
1976 my $copy = $self->copy;
1977 my $patron = $self->patron;
1978 my $duration_date_ceiling;
1979 my $duration_date_ceiling_force;
1983 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1984 $duration_date_ceiling = $policy->{duration_date_ceiling};
1985 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1987 my $dname = $duration->name;
1988 my $mname = $max->name;
1989 my $rname = $recurring->name;
1991 if($hard_due_date) {
1992 $hdname = $hard_due_date->name;
1995 $logger->debug("circulator: building circulation ".
1996 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1998 $circ->duration($policy->{duration});
1999 $circ->recurring_fine($policy->{recurring_fine});
2000 $circ->duration_rule($duration->name);
2001 $circ->recurring_fine_rule($recurring->name);
2002 $circ->max_fine_rule($max->name);
2003 $circ->max_fine($policy->{max_fine});
2004 $circ->fine_interval($recurring->recurrence_interval);
2005 $circ->renewal_remaining($duration->max_renewals);
2006 $circ->grace_period($policy->{grace_period});
2010 $logger->info("circulator: copy found with an unlimited circ duration");
2011 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2012 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2013 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2014 $circ->renewal_remaining(0);
2015 $circ->grace_period(0);
2018 $circ->target_copy( $copy->id );
2019 $circ->usr( $patron->id );
2020 $circ->circ_lib( $self->circ_lib );
2021 $circ->workstation($self->editor->requestor->wsid)
2022 if defined $self->editor->requestor->wsid;
2024 # renewals maintain a link to the parent circulation
2025 $circ->parent_circ($self->parent_circ);
2027 if( $self->is_renewal ) {
2028 $circ->opac_renewal('t') if $self->opac_renewal;
2029 $circ->phone_renewal('t') if $self->phone_renewal;
2030 $circ->desk_renewal('t') if $self->desk_renewal;
2031 $circ->renewal_remaining($self->renewal_remaining);
2032 $circ->circ_staff($self->editor->requestor->id);
2036 # if the user provided an overiding checkout time,
2037 # (e.g. the checkout really happened several hours ago), then
2038 # we apply that here. Does this need a perm??
2039 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2040 if $self->checkout_time;
2042 # if a patron is renewing, 'requestor' will be the patron
2043 $circ->circ_staff($self->editor->requestor->id);
2044 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
2049 sub do_reservation_pickup {
2052 $self->log_me("do_reservation_pickup()");
2054 $self->reservation->pickup_time('now');
2057 $self->reservation->current_resource &&
2058 $U->is_true($self->reservation->target_resource_type->catalog_item)
2060 # We used to try to set $self->copy and $self->patron here,
2061 # but that should already be done.
2063 $self->run_checkout_scripts(1);
2065 my $duration = $self->duration_rule;
2066 my $max = $self->max_fine_rule;
2067 my $recurring = $self->recurring_fines_rule;
2069 if ($duration && $max && $recurring) {
2070 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2072 my $dname = $duration->name;
2073 my $mname = $max->name;
2074 my $rname = $recurring->name;
2076 $logger->debug("circulator: updating reservation ".
2077 "with duration=$dname, maxfine=$mname, recurring=$rname");
2079 $self->reservation->fine_amount($policy->{recurring_fine});
2080 $self->reservation->max_fine($policy->{max_fine});
2081 $self->reservation->fine_interval($recurring->recurrence_interval);
2084 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2085 $self->update_copy();
2088 $self->reservation->fine_amount(
2089 $self->reservation->target_resource_type->fine_amount
2091 $self->reservation->max_fine(
2092 $self->reservation->target_resource_type->max_fine
2094 $self->reservation->fine_interval(
2095 $self->reservation->target_resource_type->fine_interval
2099 $self->update_reservation();
2102 sub do_reservation_return {
2104 my $request = shift;
2106 $self->log_me("do_reservation_return()");
2108 if (not ref $self->reservation) {
2109 my ($reservation, $evt) =
2110 $U->fetch_booking_reservation($self->reservation);
2111 return $self->bail_on_events($evt) if $evt;
2112 $self->reservation($reservation);
2115 $self->generate_fines(1);
2116 $self->reservation->return_time('now');
2117 $self->update_reservation();
2118 $self->reshelve_copy if $self->copy;
2120 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2121 $self->copy( $self->reservation->current_resource->catalog_item );
2125 sub booking_adjusted_due_date {
2127 my $circ = $self->circ;
2128 my $copy = $self->copy;
2130 return undef unless $self->use_booking;
2134 if( $self->due_date ) {
2136 return $self->bail_on_events($self->editor->event)
2137 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2139 $circ->due_date(cleanse_ISO8601($self->due_date));
2143 return unless $copy and $circ->due_date;
2146 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2147 if (@$booking_items) {
2148 my $booking_item = $booking_items->[0];
2149 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2151 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2152 my $shorten_circ_setting = $resource_type->elbow_room ||
2153 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2156 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2157 my $bookings = $booking_ses->request(
2158 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2159 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2161 $booking_ses->disconnect;
2163 my $dt_parser = DateTime::Format::ISO8601->new;
2164 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2166 for my $bid (@$bookings) {
2168 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2170 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2171 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2173 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2174 if ($booking_start < DateTime->now);
2177 if ($U->is_true($stop_circ_setting)) {
2178 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2180 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2181 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2184 # We set the circ duration here only to affect the logic that will
2185 # later (in a DB trigger) mangle the time part of the due date to
2186 # 11:59pm. Having any circ duration that is not a whole number of
2187 # days is enough to prevent the "correction."
2188 my $new_circ_duration = $due_date->epoch - time;
2189 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2190 $circ->duration("$new_circ_duration seconds");
2192 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2196 return $self->bail_on_events($self->editor->event)
2197 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2203 sub apply_modified_due_date {
2205 my $shift_earlier = shift;
2206 my $circ = $self->circ;
2207 my $copy = $self->copy;
2209 if( $self->due_date ) {
2211 return $self->bail_on_events($self->editor->event)
2212 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2214 $circ->due_date(cleanse_ISO8601($self->due_date));
2218 # if the due_date lands on a day when the location is closed
2219 return unless $copy and $circ->due_date;
2221 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2223 # due-date overlap should be determined by the location the item
2224 # is checked out from, not the owning or circ lib of the item
2225 my $org = $self->circ_lib;
2227 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2228 " with an item due date of ".$circ->due_date );
2230 my $dateinfo = $U->storagereq(
2231 'open-ils.storage.actor.org_unit.closed_date.overlap',
2232 $org, $circ->due_date );
2235 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2236 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2238 # XXX make the behavior more dynamic
2239 # for now, we just push the due date to after the close date
2240 if ($shift_earlier) {
2241 $circ->due_date($dateinfo->{start});
2243 $circ->due_date($dateinfo->{end});
2251 sub create_due_date {
2252 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2254 # if there is a raw time component (e.g. from postgres),
2255 # turn it into an interval that interval_to_seconds can parse
2256 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2258 # for now, use the server timezone. TODO: use workstation org timezone
2259 my $due_date = DateTime->now(time_zone => 'local');
2261 # add the circ duration
2262 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2265 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2266 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2267 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2272 # return ISO8601 time with timezone
2273 return $due_date->strftime('%FT%T%z');
2278 sub make_precat_copy {
2280 my $copy = $self->copy;
2283 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2285 $copy->editor($self->editor->requestor->id);
2286 $copy->edit_date('now');
2287 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2288 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2289 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2290 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2291 $self->update_copy();
2295 $logger->info("circulator: Creating a new precataloged ".
2296 "copy in checkout with barcode " . $self->copy_barcode);
2298 $copy = Fieldmapper::asset::copy->new;
2299 $copy->circ_lib($self->circ_lib);
2300 $copy->creator($self->editor->requestor->id);
2301 $copy->editor($self->editor->requestor->id);
2302 $copy->barcode($self->copy_barcode);
2303 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2304 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2305 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2307 $copy->dummy_title($self->dummy_title || "");
2308 $copy->dummy_author($self->dummy_author || "");
2309 $copy->dummy_isbn($self->dummy_isbn || "");
2310 $copy->circ_modifier($self->circ_modifier);
2313 # See if we need to override the circ_lib for the copy with a configured circ_lib
2314 # Setting is shortname of the org unit
2315 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2316 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2318 if($precat_circ_lib) {
2319 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2322 $self->bail_on_events($self->editor->event);
2326 $copy->circ_lib($org->id);
2330 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2332 $self->push_events($self->editor->event);
2336 # this is a little bit of a hack, but we need to
2337 # get the copy into the script runner
2338 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2342 sub checkout_noncat {
2348 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2349 my $count = $self->noncat_count || 1;
2350 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2352 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2356 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2357 $self->editor->requestor->id,
2365 $self->push_events($evt);
2373 # If a copy goes into transit and is then checked in before the transit checkin
2374 # interval has expired, push an event onto the overridable events list.
2375 sub check_transit_checkin_interval {
2378 # only concerned with in-transit items
2379 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2381 # no interval, no problem
2382 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2383 return unless $interval;
2385 # capture the transit so we don't have to fetch it again later during checkin
2387 $self->editor->search_action_transit_copy(
2388 {target_copy => $self->copy->id, dest_recv_time => undef}
2392 # transit from X to X for whatever reason has no min interval
2393 return if $self->transit->source == $self->transit->dest;
2395 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2396 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2397 my $horizon = $t_start->add(seconds => $seconds);
2399 # See if we are still within the transit checkin forbidden range
2400 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2401 if $horizon > DateTime->now;
2404 # Retarget local holds at checkin
2405 sub checkin_retarget {
2407 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2408 return unless $self->is_checkin; # Renewals need not be checked
2409 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2410 return if $self->is_precat; # No holds for precats
2411 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2412 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2413 my $status = $U->copy_status($self->copy->status);
2414 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2415 # Specifically target items that are likely new (by status ID)
2416 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2417 my $location = $self->copy->location;
2418 if(!ref($location)) {
2419 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2420 $self->copy->location($location);
2422 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2424 # Fetch holds for the bib
2425 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2426 $self->editor->authtoken,
2429 capture_time => undef, # No touching captured holds
2430 frozen => 'f', # Don't bother with frozen holds
2431 pickup_lib => $self->circ_lib # Only holds actually here
2434 # Error? Skip the step.
2435 return if exists $result->{"ilsevent"};
2439 foreach my $holdlist (keys %{$result}) {
2440 push @$holds, @{$result->{$holdlist}};
2443 return if scalar(@$holds) == 0; # No holds, no retargeting
2445 # Check for parts on this copy
2446 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2447 my %parts_hash = ();
2448 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2450 # Loop over holds in request-ish order
2451 # Stage 1: Get them into request-ish order
2452 # Also grab type and target for skipping low hanging ones
2453 $result = $self->editor->json_query({
2454 "select" => { "ahr" => ["id", "hold_type", "target"] },
2455 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2456 "where" => { "id" => $holds },
2458 { "class" => "pgt", "field" => "hold_priority"},
2459 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2460 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2461 { "class" => "ahr", "field" => "request_time"}
2466 if (ref $result eq "ARRAY" and scalar @$result) {
2467 foreach (@{$result}) {
2468 # Copy level, but not this copy?
2469 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2470 and $_->{target} != $self->copy->id);
2471 # Volume level, but not this volume?
2472 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2473 if(@$parts) { # We have parts?
2475 next if ($_->{hold_type} eq 'T');
2476 # Skip part holds for parts not on this copy
2477 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2479 # No parts, no part holds
2480 next if ($_->{hold_type} eq 'P');
2482 # So much for easy stuff, attempt a retarget!
2483 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2484 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2485 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2493 $self->log_me("do_checkin()");
2495 return $self->bail_on_events(
2496 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2499 $self->check_transit_checkin_interval;
2500 $self->checkin_retarget;
2502 # the renew code and mk_env should have already found our circulation object
2503 unless( $self->circ ) {
2505 my $circs = $self->editor->search_action_circulation(
2506 { target_copy => $self->copy->id, checkin_time => undef });
2508 $self->circ($$circs[0]);
2510 # for now, just warn if there are multiple open circs on a copy
2511 $logger->warn("circulator: we have ".scalar(@$circs).
2512 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2515 # run the fine generator against this circ, if this circ is there
2516 $self->generate_fines_start if $self->circ;
2518 if( $self->checkin_check_holds_shelf() ) {
2519 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2520 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2521 if($self->fake_hold_dest) {
2522 $self->hold->pickup_lib($self->circ_lib);
2524 $self->checkin_flesh_events;
2528 unless( $self->is_renewal ) {
2529 return $self->bail_on_events($self->editor->event)
2530 unless $self->editor->allowed('COPY_CHECKIN');
2533 $self->push_events($self->check_copy_alert());
2534 $self->push_events($self->check_checkin_copy_status());
2536 # if the circ is marked as 'claims returned', add the event to the list
2537 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2538 if ($self->circ and $self->circ->stop_fines
2539 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2541 $self->check_circ_deposit();
2543 # handle the overridable events
2544 $self->override_events unless $self->is_renewal;
2545 return if $self->bail_out;
2547 if( $self->copy and !$self->transit ) {
2549 $self->editor->search_action_transit_copy(
2550 { target_copy => $self->copy->id, dest_recv_time => undef }
2556 $self->generate_fines_finish;
2557 $self->checkin_handle_circ;
2558 return if $self->bail_out;
2559 $self->checkin_changed(1);
2561 } elsif( $self->transit ) {
2562 my $hold_transit = $self->process_received_transit;
2563 $self->checkin_changed(1);
2565 if( $self->bail_out ) {
2566 $self->checkin_flesh_events;
2570 if( my $e = $self->check_checkin_copy_status() ) {
2571 # If the original copy status is special, alert the caller
2572 my $ev = $self->events;
2573 $self->events([$e]);
2574 $self->override_events;
2575 return if $self->bail_out;
2579 if( $hold_transit or
2580 $U->copy_status($self->copy->status)->id
2581 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2584 if( $hold_transit ) {
2585 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2587 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2592 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2594 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2595 $self->reshelve_copy(1);
2596 $self->cancelled_hold_transit(1);
2597 $self->notify_hold(0); # don't notify for cancelled holds
2598 $self->fake_hold_dest(0);
2599 return if $self->bail_out;
2601 } elsif ($hold and $hold->hold_type eq 'R') {
2603 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2604 $self->notify_hold(0); # No need to notify
2605 $self->fake_hold_dest(0);
2606 $self->noop(1); # Don't try and capture for other holds/transits now
2607 $self->update_copy();
2608 $hold->fulfillment_time('now');
2609 $self->bail_on_events($self->editor->event)
2610 unless $self->editor->update_action_hold_request($hold);
2614 # hold transited to correct location
2615 if($self->fake_hold_dest) {
2616 $hold->pickup_lib($self->circ_lib);
2618 $self->checkin_flesh_events;
2623 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2625 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2626 " that is in-transit, but there is no transit.. repairing");
2627 $self->reshelve_copy(1);
2628 return if $self->bail_out;
2631 if( $self->is_renewal ) {
2632 $self->finish_fines_and_voiding;
2633 return if $self->bail_out;
2634 $self->push_events(OpenILS::Event->new('SUCCESS'));
2638 # ------------------------------------------------------------------------------
2639 # Circulations and transits are now closed where necessary. Now go on to see if
2640 # this copy can fulfill a hold or needs to be routed to a different location
2641 # ------------------------------------------------------------------------------
2643 my $needed_for_something = 0; # formerly "needed_for_hold"
2645 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2647 if (!$self->remote_hold) {
2648 if ($self->use_booking) {
2649 my $potential_hold = $self->hold_capture_is_possible;
2650 my $potential_reservation = $self->reservation_capture_is_possible;
2652 if ($potential_hold and $potential_reservation) {
2653 $logger->info("circulator: item could fulfill either hold or reservation");
2654 $self->push_events(new OpenILS::Event(
2655 "HOLD_RESERVATION_CONFLICT",
2656 "hold" => $potential_hold,
2657 "reservation" => $potential_reservation
2659 return if $self->bail_out;
2660 } elsif ($potential_hold) {
2661 $needed_for_something =
2662 $self->attempt_checkin_hold_capture;
2663 } elsif ($potential_reservation) {
2664 $needed_for_something =
2665 $self->attempt_checkin_reservation_capture;
2668 $needed_for_something = $self->attempt_checkin_hold_capture;
2671 return if $self->bail_out;
2673 unless($needed_for_something) {
2674 my $circ_lib = (ref $self->copy->circ_lib) ?
2675 $self->copy->circ_lib->id : $self->copy->circ_lib;
2677 if( $self->remote_hold ) {
2678 $circ_lib = $self->remote_hold->pickup_lib;
2679 $logger->warn("circulator: Copy ".$self->copy->barcode.
2680 " is on a remote hold's shelf, sending to $circ_lib");
2683 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2685 my $suppress_transit = 0;
2687 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2688 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2689 if($suppress_transit_source && $suppress_transit_source->{value}) {
2690 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2691 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2692 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2693 $suppress_transit = 1;
2698 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2699 # copy is where it needs to be, either for hold or reshelving
2701 $self->checkin_handle_precat();
2702 return if $self->bail_out;
2705 # copy needs to transit "home", or stick here if it's a floating copy
2707 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2708 $self->checkin_changed(1);
2709 $self->copy->circ_lib( $self->circ_lib );
2712 my $bc = $self->copy->barcode;
2713 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2714 $self->checkin_build_copy_transit($circ_lib);
2715 return if $self->bail_out;
2716 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2720 } else { # no-op checkin
2721 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2722 $self->checkin_changed(1);
2723 $self->copy->circ_lib( $self->circ_lib );
2728 if($self->claims_never_checked_out and
2729 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2731 # the item was not supposed to be checked out to the user and should now be marked as missing
2732 $self->copy->status(OILS_COPY_STATUS_MISSING);
2736 $self->reshelve_copy unless $needed_for_something;
2739 return if $self->bail_out;
2741 unless($self->checkin_changed) {
2743 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2744 my $stat = $U->copy_status($self->copy->status)->id;
2746 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2747 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2748 $self->bail_out(1); # no need to commit anything
2752 $self->push_events(OpenILS::Event->new('SUCCESS'))
2753 unless @{$self->events};
2756 $self->finish_fines_and_voiding;
2758 OpenILS::Utils::Penalty->calculate_penalties(
2759 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2761 $self->checkin_flesh_events;
2765 sub finish_fines_and_voiding {
2767 return unless $self->circ;
2769 # gather any updates to the circ after fine generation, if there was a circ
2770 $self->generate_fines_finish;
2772 return unless $self->backdate or $self->void_overdues;
2774 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2775 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2777 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2778 $self->editor, $self->circ, $self->backdate, $note);
2780 return $self->bail_on_events($evt) if $evt;
2782 # Make sure the circ is open or closed as necessary.
2783 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2784 return $self->bail_on_events($evt) if $evt;
2790 # if a deposit was payed for this item, push the event
2791 sub check_circ_deposit {
2793 return unless $self->circ;
2794 my $deposit = $self->editor->search_money_billing(
2796 xact => $self->circ->id,
2798 }, {idlist => 1})->[0];
2800 $self->push_events(OpenILS::Event->new(
2801 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2806 my $force = $self->force || shift;
2807 my $copy = $self->copy;
2809 my $stat = $U->copy_status($copy->status)->id;
2812 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2813 $stat != OILS_COPY_STATUS_CATALOGING and
2814 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2815 $stat != OILS_COPY_STATUS_RESHELVING )) {
2817 $copy->status( OILS_COPY_STATUS_RESHELVING );
2819 $self->checkin_changed(1);
2824 # Returns true if the item is at the current location
2825 # because it was transited there for a hold and the
2826 # hold has not been fulfilled
2827 sub checkin_check_holds_shelf {
2829 return 0 unless $self->copy;
2832 $U->copy_status($self->copy->status)->id ==
2833 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2835 # Attempt to clear shelf expired holds for this copy
2836 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2837 if($self->clear_expired);
2839 # find the hold that put us on the holds shelf
2840 my $holds = $self->editor->search_action_hold_request(
2842 current_copy => $self->copy->id,
2843 capture_time => { '!=' => undef },
2844 fulfillment_time => undef,
2845 cancel_time => undef,
2850 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2851 $self->reshelve_copy(1);
2855 my $hold = $$holds[0];
2857 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2858 $hold->id. "] for copy ".$self->copy->barcode);
2860 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2861 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2862 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2863 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2864 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2865 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2866 $self->fake_hold_dest(1);
2872 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2873 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2877 $logger->info("circulator: hold is not for here..");
2878 $self->remote_hold($hold);
2883 sub checkin_handle_precat {
2885 my $copy = $self->copy;
2887 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2888 $copy->status(OILS_COPY_STATUS_CATALOGING);
2889 $self->update_copy();
2890 $self->checkin_changed(1);
2891 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2896 sub checkin_build_copy_transit {
2899 my $copy = $self->copy;
2900 my $transit = Fieldmapper::action::transit_copy->new;
2902 # if we are transiting an item to the shelf shelf, it's a hold transit
2903 if (my $hold = $self->remote_hold) {
2904 $transit = Fieldmapper::action::hold_transit_copy->new;
2905 $transit->hold($hold->id);
2907 # the item is going into transit, remove any shelf-iness
2908 if ($hold->current_shelf_lib or $hold->shelf_time) {
2909 $hold->clear_current_shelf_lib;
2910 $hold->clear_shelf_time;
2911 return $self->bail_on_events($self->editor->event)
2912 unless $self->editor->update_action_hold_request($hold);
2916 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2917 $logger->info("circulator: transiting copy to $dest");
2919 $transit->source($self->circ_lib);
2920 $transit->dest($dest);
2921 $transit->target_copy($copy->id);
2922 $transit->source_send_time('now');
2923 $transit->copy_status( $U->copy_status($copy->status)->id );
2925 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2927 if ($self->remote_hold) {
2928 return $self->bail_on_events($self->editor->event)
2929 unless $self->editor->create_action_hold_transit_copy($transit);
2931 return $self->bail_on_events($self->editor->event)
2932 unless $self->editor->create_action_transit_copy($transit);
2935 # ensure the transit is returned to the caller
2936 $self->transit($transit);
2938 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2940 $self->checkin_changed(1);
2944 sub hold_capture_is_possible {
2946 my $copy = $self->copy;
2948 # we've been explicitly told not to capture any holds
2949 return 0 if $self->capture eq 'nocapture';
2951 # See if this copy can fulfill any holds
2952 my $hold = $holdcode->find_nearest_permitted_hold(
2953 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2955 return undef if ref $hold eq "HASH" and
2956 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2960 sub reservation_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 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2968 my $resv = $booking_ses->request(
2969 "open-ils.booking.reservations.could_capture",
2970 $self->editor->authtoken, $copy->barcode
2972 $booking_ses->disconnect;
2973 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2974 $self->push_events($resv);
2980 # returns true if the item was used (or may potentially be used
2981 # in subsequent calls) to capture a hold.
2982 sub attempt_checkin_hold_capture {
2984 my $copy = $self->copy;
2986 # we've been explicitly told not to capture any holds
2987 return 0 if $self->capture eq 'nocapture';
2989 # See if this copy can fulfill any holds
2990 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2991 $self->editor, $copy, $self->editor->requestor );
2994 $logger->debug("circulator: no potential permitted".
2995 "holds found for copy ".$copy->barcode);
2999 if($self->capture ne 'capture') {
3000 # see if this item is in a hold-capture-delay location
3001 my $location = $self->copy->location;
3002 if(!ref($location)) {
3003 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3004 $self->copy->location($location);
3006 if($U->is_true($location->hold_verify)) {
3007 $self->bail_on_events(
3008 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3013 $self->retarget($retarget);
3015 my $suppress_transit = 0;
3016 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3017 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3018 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3019 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3020 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3021 $suppress_transit = 1;
3022 $self->hold->pickup_lib($self->circ_lib);
3027 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3029 $hold->current_copy($copy->id);
3030 $hold->capture_time('now');
3031 $self->put_hold_on_shelf($hold)
3032 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3034 # prevent DB errors caused by fetching
3035 # holds from storage, and updating through cstore
3036 $hold->clear_fulfillment_time;
3037 $hold->clear_fulfillment_staff;
3038 $hold->clear_fulfillment_lib;
3039 $hold->clear_expire_time;
3040 $hold->clear_cancel_time;
3041 $hold->clear_prev_check_time unless $hold->prev_check_time;
3043 $self->bail_on_events($self->editor->event)
3044 unless $self->editor->update_action_hold_request($hold);
3046 $self->checkin_changed(1);
3048 return 0 if $self->bail_out;
3050 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3052 if ($hold->hold_type eq 'R') {
3053 $copy->status(OILS_COPY_STATUS_CATALOGING);
3054 $hold->fulfillment_time('now');
3055 $self->noop(1); # Block other transit/hold checks
3056 $self->bail_on_events($self->editor->event)
3057 unless $self->editor->update_action_hold_request($hold);
3059 # This hold was captured in the correct location
3060 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3061 $self->push_events(OpenILS::Event->new('SUCCESS'));
3063 #$self->do_hold_notify($hold->id);
3064 $self->notify_hold($hold->id);
3069 # Hold needs to be picked up elsewhere. Build a hold
3070 # transit and route the item.
3071 $self->checkin_build_hold_transit();
3072 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3073 return 0 if $self->bail_out;
3074 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3077 # make sure we save the copy status
3079 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3083 sub attempt_checkin_reservation_capture {
3085 my $copy = $self->copy;
3087 # we've been explicitly told not to capture any holds
3088 return 0 if $self->capture eq 'nocapture';
3090 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3091 my $evt = $booking_ses->request(
3092 "open-ils.booking.resources.capture_for_reservation",
3093 $self->editor->authtoken,
3095 1 # don't update copy - we probably have it locked
3097 $booking_ses->disconnect;
3099 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3101 "open-ils.booking.resources.capture_for_reservation " .
3102 "didn't return an event!"
3106 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3107 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3109 # not-transferable is an error event we'll pass on the user
3110 $logger->warn("reservation capture attempted against non-transferable item");
3111 $self->push_events($evt);
3113 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3114 # Re-retrieve copy as reservation capture may have changed
3115 # its status and whatnot.
3117 "circulator: booking capture win on copy " . $self->copy->id
3119 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3121 "circulator: changing copy " . $self->copy->id .
3122 "'s status from " . $self->copy->status . " to " .
3125 $self->copy->status($new_copy_status);
3128 $self->reservation($evt->{"payload"}->{"reservation"});
3130 if (exists $evt->{"payload"}->{"transit"}) {
3134 "org" => $evt->{"payload"}->{"transit"}->dest
3138 $self->checkin_changed(1);
3142 # other results are treated as "nothing to capture"
3146 sub do_hold_notify {
3147 my( $self, $holdid ) = @_;
3149 my $e = new_editor(xact => 1);
3150 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3152 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3153 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3155 $logger->info("circulator: running delayed hold notify process");
3157 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3158 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3160 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3161 hold_id => $holdid, requestor => $self->editor->requestor);
3163 $logger->debug("circulator: built hold notifier");
3165 if(!$notifier->event) {
3167 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3169 my $stat = $notifier->send_email_notify;
3170 if( $stat == '1' ) {
3171 $logger->info("circulator: hold notify succeeded for hold $holdid");
3175 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3178 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3182 sub retarget_holds {
3184 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3185 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3186 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3187 # no reason to wait for the return value
3191 sub checkin_build_hold_transit {
3194 my $copy = $self->copy;
3195 my $hold = $self->hold;
3196 my $trans = Fieldmapper::action::hold_transit_copy->new;
3198 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3200 $trans->hold($hold->id);
3201 $trans->source($self->circ_lib);
3202 $trans->dest($hold->pickup_lib);
3203 $trans->source_send_time("now");
3204 $trans->target_copy($copy->id);
3206 # when the copy gets to its destination, it will recover
3207 # this status - put it onto the holds shelf
3208 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3210 return $self->bail_on_events($self->editor->event)
3211 unless $self->editor->create_action_hold_transit_copy($trans);
3216 sub process_received_transit {
3218 my $copy = $self->copy;
3219 my $copyid = $self->copy->id;
3221 my $status_name = $U->copy_status($copy->status)->name;
3222 $logger->debug("circulator: attempting transit receive on ".
3223 "copy $copyid. Copy status is $status_name");
3225 my $transit = $self->transit;
3227 # Check if we are in a transit suppress range
3228 my $suppress_transit = 0;
3229 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3230 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3231 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3232 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3233 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3234 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3235 $suppress_transit = 1;
3236 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3240 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3241 # - this item is in-transit to a different location
3242 # - Or we are capturing holds as transits, so why create a new transit?
3244 my $tid = $transit->id;
3245 my $loc = $self->circ_lib;
3246 my $dest = $transit->dest;
3248 $logger->info("circulator: Fowarding transit on copy which is destined ".
3249 "for a different location. transit=$tid, copy=$copyid, current ".
3250 "location=$loc, destination location=$dest");
3252 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3254 # grab the associated hold object if available
3255 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3256 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3258 return $self->bail_on_events($evt);
3261 # The transit is received, set the receive time
3262 $transit->dest_recv_time('now');
3263 $self->bail_on_events($self->editor->event)
3264 unless $self->editor->update_action_transit_copy($transit);
3266 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3268 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3269 $copy->status( $transit->copy_status );
3270 $self->update_copy();
3271 return if $self->bail_out;
3275 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3278 # hold has arrived at destination, set shelf time
3279 $self->put_hold_on_shelf($hold);
3280 $self->bail_on_events($self->editor->event)
3281 unless $self->editor->update_action_hold_request($hold);
3282 return if $self->bail_out;
3284 $self->notify_hold($hold_transit->hold);
3287 $hold_transit = undef;
3288 $self->cancelled_hold_transit(1);
3289 $self->reshelve_copy(1);
3290 $self->fake_hold_dest(0);
3295 OpenILS::Event->new(
3298 payload => { transit => $transit, holdtransit => $hold_transit } ));
3300 return $hold_transit;
3304 # ------------------------------------------------------------------
3305 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3306 # ------------------------------------------------------------------
3307 sub put_hold_on_shelf {
3308 my($self, $hold) = @_;
3309 $hold->shelf_time('now');
3310 $hold->current_shelf_lib($self->circ_lib);
3311 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3317 sub generate_fines {
3319 my $reservation = shift;
3321 $self->generate_fines_start($reservation);
3322 $self->generate_fines_finish($reservation);
3327 sub generate_fines_start {
3329 my $reservation = shift;
3330 my $dt_parser = DateTime::Format::ISO8601->new;
3332 my $obj = $reservation ? $self->reservation : $self->circ;
3334 # If we have a grace period
3335 if($obj->can('grace_period')) {
3336 # Parse out the due date
3337 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3338 # Add the grace period to the due date
3339 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3340 # Don't generate fines on circs still in grace period
3341 return undef if ($due_date > DateTime->now);
3344 if (!exists($self->{_gen_fines_req})) {
3345 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3347 'open-ils.storage.action.circulation.overdue.generate_fines',
3355 sub generate_fines_finish {
3357 my $reservation = shift;
3359 return undef unless $self->{_gen_fines_req};
3361 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3363 $self->{_gen_fines_req}->wait_complete;
3364 delete($self->{_gen_fines_req});
3366 # refresh the circ in case the fine generator set the stop_fines field
3367 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3368 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3373 sub checkin_handle_circ {
3375 my $circ = $self->circ;
3376 my $copy = $self->copy;
3380 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3382 # backdate the circ if necessary
3383 if($self->backdate) {
3384 my $evt = $self->checkin_handle_backdate;
3385 return $self->bail_on_events($evt) if $evt;
3388 if(!$circ->stop_fines) {
3389 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3390 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3391 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3392 $circ->stop_fines_time('now');
3393 $circ->stop_fines_time($self->backdate) if $self->backdate;
3396 # Set the checkin vars since we have the item
3397 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3399 # capture the true scan time for back-dated checkins
3400 $circ->checkin_scan_time('now');
3402 $circ->checkin_staff($self->editor->requestor->id);
3403 $circ->checkin_lib($self->circ_lib);
3404 $circ->checkin_workstation($self->editor->requestor->wsid);
3406 my $circ_lib = (ref $self->copy->circ_lib) ?
3407 $self->copy->circ_lib->id : $self->copy->circ_lib;
3408 my $stat = $U->copy_status($self->copy->status)->id;
3410 if ($stat == OILS_COPY_STATUS_LOST) {
3411 # we will now handle lost fines, but the copy will retain its 'lost'
3412 # status if it needs to transit home unless lost_immediately_available
3415 # if we decide to also delay fine handling until the item arrives home,
3416 # we will need to call lost fine handling code both when checking items
3417 # in and also when receiving transits
3418 $self->checkin_handle_lost($circ_lib);
3419 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3420 # same process as above.
3421 $self->checkin_handle_long_overdue($circ_lib);
3422 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3423 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3425 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3430 # see if there are any fines owed on this circ. if not, close it
3431 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3432 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3434 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3436 return $self->bail_on_events($self->editor->event)
3437 unless $self->editor->update_action_circulation($circ);
3442 # ------------------------------------------------------------------
3443 # See if we need to void billings, etc. for lost checkin
3444 # ------------------------------------------------------------------
3445 sub checkin_handle_lost {
3447 my $circ_lib = shift;
3449 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3450 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3452 return $self->checkin_handle_lost_or_longoverdue(
3453 circ_lib => $circ_lib,
3454 max_return => $max_return,
3455 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3456 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3457 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3458 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3459 ous_use_last_activity => undef, # not supported for LOST checkin
3460 void_cost_btype => 3,
3465 # ------------------------------------------------------------------
3466 # See if we need to void billings, etc. for long-overdue checkin
3467 # note: not using constants below since they serve little purpose
3468 # for single-use strings that are descriptive in their own right
3469 # and mostly just complicate debugging.
3470 # ------------------------------------------------------------------
3471 sub checkin_handle_long_overdue {
3473 my $circ_lib = shift;
3475 $logger->info("circulator: processing long-overdue checkin...");
3477 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3478 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3480 return $self->checkin_handle_lost_or_longoverdue(
3481 circ_lib => $circ_lib,
3482 max_return => $max_return,
3483 is_longoverdue => 1,
3484 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3485 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3486 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3487 ous_immediately_available => 'circ.longoverdue_immediately_available',
3488 ous_use_last_activity =>
3489 'circ.longoverdue.use_last_activity_date_on_return',
3490 void_cost_btype => 10,
3491 void_fee_btype => 11
3495 # last billing activity is last payment time, last billing time, or the
3496 # circ due date. If the relevant "use last activity" org unit setting is
3497 # false/unset, then last billing activity is always the due date.
3498 sub get_circ_last_billing_activity {
3500 my $circ_lib = shift;
3501 my $setting = shift;
3502 my $date = $self->circ->due_date;
3504 return $date unless $setting and
3505 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3507 my $xact = $self->editor->retrieve_money_billable_transaction([
3509 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3512 if ($xact->summary) {
3513 $date = $xact->summary->last_payment_ts ||
3514 $xact->summary->last_billing_ts ||
3515 $self->circ->due_date;
3522 sub checkin_handle_lost_or_longoverdue {
3523 my ($self, %args) = @_;
3525 my $circ = $self->circ;
3526 my $max_return = $args{max_return};
3527 my $circ_lib = $args{circ_lib};
3532 $self->get_circ_last_billing_activity(
3533 $circ_lib, $args{ous_use_last_activity});
3536 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3537 $tm[5] -= 1 if $tm[5] > 0;
3538 my $due = timelocal(int($tm[1]), int($tm[2]),
3539 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3542 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3544 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3545 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3546 "DUE: $due LAST: $last_chance");
3548 $max_return = 0 if $today < $last_chance;
3554 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3555 "return interval. skipping fine/fee voiding, etc.");
3557 } else { # within max-return interval or no interval defined
3559 $logger->info("circulator: check-in of lost/lo item is within the ".
3560 "max return interval (or no interval is defined). Proceeding ".
3561 "with fine/fee voiding, etc.");
3563 my $void_cost = $U->ou_ancestor_setting_value(
3564 $circ_lib, $args{ous_void_item_cost}, $self->editor) || 0;
3565 my $void_proc_fee = $U->ou_ancestor_setting_value(
3566 $circ_lib, $args{ous_void_proc_fee}, $self->editor) || 0;
3567 my $restore_od = $U->ou_ancestor_setting_value(
3568 $circ_lib, $args{ous_restore_overdue}, $self->editor) || 0;
3570 # for reference: generate-overdues-on-long-overdue-checkin is not
3571 # supported because it doesn't make any sense that a circ would be
3572 # marked as long-overdue before it was done being regular-overdue
3573 if (!$args{is_longoverdue}) {
3574 $self->generate_lost_overdue(1) if
3575 $U->ou_ancestor_setting_value($circ_lib,
3576 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
3580 $self->checkin_handle_lost_or_lo_now_found(
3581 $args{void_cost_btype}, $args{is_longoverdue}) if $void_cost;
3582 $self->checkin_handle_lost_or_lo_now_found(
3583 $args{void_fee_btype}, $args{is_longoverdue}) if $void_proc_fee;
3584 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3585 if $restore_od && ! $self->void_overdues;
3588 if ($circ_lib != $self->circ_lib) {
3589 # if the item is not home, check to see if we want to retain the
3590 # lost/longoverdue status at this point in the process
3592 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3593 $args{ous_immediately_available}, $self->editor) || 0;
3595 if ($immediately_available) {
3596 # item status does not need to be retained, so give it a
3597 # reshelving status as if it were a normal checkin
3598 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3601 $logger->info("circulator: leaving lost/longoverdue copy".
3602 " status in place on checkin");
3605 # lost/longoverdue item is home and processed, treat like a normal
3606 # checkin from this point on
3607 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3613 sub checkin_handle_backdate {
3616 # ------------------------------------------------------------------
3617 # clean up the backdate for date comparison
3618 # XXX We are currently taking the due-time from the original due-date,
3619 # not the input. Do we need to do this? This certainly interferes with
3620 # backdating of hourly checkouts, but that is likely a very rare case.
3621 # ------------------------------------------------------------------
3622 my $bd = cleanse_ISO8601($self->backdate);
3623 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3624 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3625 $new_date->set_hour($original_date->hour());
3626 $new_date->set_minute($original_date->minute());
3627 $bd = cleanse_ISO8601($new_date->datetime());
3629 $self->backdate($bd);
3634 sub check_checkin_copy_status {
3636 my $copy = $self->copy;
3638 my $status = $U->copy_status($copy->status)->id;
3641 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3642 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3643 $status == OILS_COPY_STATUS_IN_PROCESS ||
3644 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3645 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3646 $status == OILS_COPY_STATUS_CATALOGING ||
3647 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3648 $status == OILS_COPY_STATUS_RESHELVING );
3650 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3651 if( $status == OILS_COPY_STATUS_LOST );
3653 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3654 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3656 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3657 if( $status == OILS_COPY_STATUS_MISSING );
3659 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3664 # --------------------------------------------------------------------------
3665 # On checkin, we need to return as many relevant objects as we can
3666 # --------------------------------------------------------------------------
3667 sub checkin_flesh_events {
3670 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3671 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3672 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3675 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3678 if($self->hold and !$self->hold->cancel_time) {
3679 $hold = $self->hold;
3680 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3684 # update our copy of the circ object and
3685 # flesh the billing summary data
3687 $self->editor->retrieve_action_circulation([
3691 circ => ['billable_transaction'],
3700 # flesh some patron fields before returning
3702 $self->editor->retrieve_actor_user([
3707 au => ['card', 'billing_address', 'mailing_address']
3714 for my $evt (@{$self->events}) {
3717 $payload->{copy} = $U->unflesh_copy($self->copy);
3718 $payload->{volume} = $self->volume;
3719 $payload->{record} = $record,
3720 $payload->{circ} = $self->circ;
3721 $payload->{transit} = $self->transit;
3722 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3723 $payload->{hold} = $hold;
3724 $payload->{patron} = $self->patron;
3725 $payload->{reservation} = $self->reservation
3726 unless (not $self->reservation or $self->reservation->cancel_time);
3728 $evt->{payload} = $payload;
3733 my( $self, $msg ) = @_;
3734 my $bc = ($self->copy) ? $self->copy->barcode :
3737 my $usr = ($self->patron) ? $self->patron->id : "";
3738 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3739 ", recipient=$usr, copy=$bc");
3745 $self->log_me("do_renew()");
3747 # Make sure there is an open circ to renew that is not
3748 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3749 my $usrid = $self->patron->id if $self->patron;
3750 my $circ = $self->editor->search_action_circulation({
3751 target_copy => $self->copy->id,
3752 xact_finish => undef,
3753 checkin_time => undef,
3754 ($usrid ? (usr => $usrid) : ()),
3756 {stop_fines => undef},
3757 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3761 return $self->bail_on_events($self->editor->event) unless $circ;
3763 # A user is not allowed to renew another user's items without permission
3764 unless( $circ->usr eq $self->editor->requestor->id ) {
3765 return $self->bail_on_events($self->editor->events)
3766 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3769 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3770 if $circ->renewal_remaining < 1;
3772 # -----------------------------------------------------------------
3774 $self->parent_circ($circ->id);
3775 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3778 # Opac renewal - re-use circ library from original circ (unless told not to)
3779 if($self->opac_renewal) {
3780 unless(defined($opac_renewal_use_circ_lib)) {
3781 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3782 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3783 $opac_renewal_use_circ_lib = 1;
3786 $opac_renewal_use_circ_lib = 0;
3789 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3792 # Desk renewal - re-use circ library from original circ (unless told not to)
3793 if($self->desk_renewal) {
3794 unless(defined($desk_renewal_use_circ_lib)) {
3795 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3796 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3797 $desk_renewal_use_circ_lib = 1;
3800 $desk_renewal_use_circ_lib = 0;
3803 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3806 # Run the fine generator against the old circ
3807 $self->generate_fines_start;
3809 $self->run_renew_permit;
3812 $self->do_checkin();
3813 return if $self->bail_out;
3815 unless( $self->permit_override ) {
3817 return if $self->bail_out;
3818 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3819 $self->remove_event('ITEM_NOT_CATALOGED');
3822 $self->override_events;
3823 return if $self->bail_out;
3826 $self->do_checkout();
3831 my( $self, $evt ) = @_;
3832 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3833 $logger->debug("circulator: removing event from list: $evt");
3834 my @events = @{$self->events};
3835 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3840 my( $self, $evt ) = @_;
3841 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3842 return grep { $_->{textcode} eq $evt } @{$self->events};
3847 sub run_renew_permit {
3850 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3851 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3852 $self->editor, $self->copy, $self->editor->requestor, 1
3854 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3857 if(!$self->legacy_script_support) {
3858 my $results = $self->run_indb_circ_test;
3859 $self->push_events($self->matrix_test_result_events)
3860 unless $self->circ_test_success;
3863 my $runner = $self->script_runner;
3865 $runner->load($self->circ_permit_renew);
3866 my $result = $runner->run or
3867 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3868 if ($result->{"events"}) {
3870 map { new OpenILS::Event($_) } @{$result->{"events"}}
3873 "circulator: circ_permit_renew for user " .
3874 $self->patron->id . " returned " .
3875 scalar(@{$result->{"events"}}) . " event(s)"
3879 $self->mk_script_runner;
3882 $logger->debug("circulator: re-creating script runner to be safe");
3886 # XXX: The primary mechanism for storing circ history is now handled
3887 # by tracking real circulation objects instead of bibs in a bucket.
3888 # However, this code is disabled by default and could be useful
3889 # some day, so may as well leave it for now.
3890 sub append_reading_list {
3894 $self->is_checkout and
3900 # verify history is globally enabled and uses the bucket mechanism
3901 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3902 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3904 return undef unless $htype and $htype eq 'bucket';
3906 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3908 # verify the patron wants to retain the hisory
3909 my $setting = $e->search_actor_user_setting(
3910 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3912 unless($setting and $setting->value) {
3917 my $bkt = $e->search_container_copy_bucket(
3918 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3923 # find the next item position
3924 my $last_item = $e->search_container_copy_bucket_item(
3925 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3926 $pos = $last_item->pos + 1 if $last_item;
3929 # create the history bucket if necessary
3930 $bkt = Fieldmapper::container::copy_bucket->new;
3931 $bkt->owner($self->patron->id);
3933 $bkt->btype('circ_history');
3935 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3938 my $item = Fieldmapper::container::copy_bucket_item->new;
3940 $item->bucket($bkt->id);
3941 $item->target_copy($self->copy->id);
3944 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3951 sub make_trigger_events {
3953 return unless $self->circ;
3954 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3955 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3956 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3961 sub checkin_handle_lost_or_lo_now_found {
3962 my ($self, $bill_type, $is_longoverdue) = @_;
3964 # ------------------------------------------------------------------
3965 # remove charge from patron's account if lost item is returned
3966 # ------------------------------------------------------------------
3968 my $bills = $self->editor->search_money_billing(
3970 xact => $self->circ->id,
3975 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3977 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
3978 for my $bill (@$bills) {
3979 if( !$U->is_true($bill->voided) ) {
3980 $logger->info("$tag item returned - voiding bill ".$bill->id);
3982 $bill->void_time('now');
3983 $bill->voider($self->editor->requestor->id);
3984 my $note = ($bill->note) ? $bill->note . "\n" : '';
3985 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
3987 $self->bail_on_events($self->editor->event)
3988 unless $self->editor->update_money_billing($bill);
3993 sub checkin_handle_lost_or_lo_now_found_restore_od {
3995 my $circ_lib = shift;
3996 my $is_longoverdue = shift;
3997 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3999 # ------------------------------------------------------------------
4000 # restore those overdue charges voided when item was set to lost
4001 # ------------------------------------------------------------------
4003 my $ods = $self->editor->search_money_billing(
4005 xact => $self->circ->id,
4010 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4011 for my $bill (@$ods) {
4012 if( $U->is_true($bill->voided) ) {
4013 $logger->info("$tag item returned - restoring overdue ".$bill->id);
4015 $bill->clear_void_time;
4016 $bill->voider($self->editor->requestor->id);
4017 my $note = ($bill->note) ? $bill->note . "\n" : '';
4018 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
4020 $self->bail_on_events($self->editor->event)
4021 unless $self->editor->update_money_billing($bill);
4026 # ------------------------------------------------------------------
4027 # Lost-then-found item checked in. This sub generates new overdue
4028 # fines, beyond the point of any existing and possibly voided
4029 # overdue fines, up to the point of final checkin time (or max fine
4031 # ------------------------------------------------------------------
4032 sub generate_lost_overdue_fines {
4034 my $circ = $self->circ;
4035 my $e = $self->editor;
4037 # Re-open the transaction so the fine generator can see it
4038 if($circ->xact_finish or $circ->stop_fines) {
4040 $circ->clear_xact_finish;
4041 $circ->clear_stop_fines;
4042 $circ->clear_stop_fines_time;
4043 $e->update_action_circulation($circ) or return $e->die_event;
4047 $e->xact_begin; # generate_fines expects an in-xact editor
4048 $self->generate_fines;
4049 $circ = $self->circ; # generate fines re-fetches the circ
4053 # Re-close the transaction if no money is owed
4054 my ($obt) = $U->fetch_mbts($circ->id, $e);
4055 if ($obt and $obt->balance_owed == 0) {
4056 $circ->xact_finish('now');
4060 # Set stop fines if the fine generator didn't have to
4061 unless($circ->stop_fines) {
4062 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
4063 $circ->stop_fines_time('now');
4067 # update the event data sent to the caller within the transaction
4068 $self->checkin_flesh_events;
4071 $e->update_action_circulation($circ) or return $e->die_event;