1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
11 my $U = "OpenILS::Application::AppUtils";
15 my $legacy_script_support = 0;
17 my $opac_renewal_use_circ_lib;
19 sub determine_booking_status {
20 unless (defined $booking_status) {
21 my $ses = create OpenSRF::AppSession("router");
22 $booking_status = grep {$_ eq "open-ils.booking"} @{
23 $ses->request("opensrf.router.info.class.list")->gather(1)
26 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
29 return $booking_status;
35 flesh_fields => {acp => ['call_number','parts'], acn => ['record']}
41 my $conf = OpenSRF::Utils::SettingsClient->new;
42 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
44 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
45 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
47 my $lb = $conf->config_value( @pfx2, 'script_path' );
48 $lb = [ $lb ] unless ref($lb);
51 return unless $legacy_script_support;
53 my @pfx = ( @pfx2, "scripts" );
54 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
55 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
56 my $d = $conf->config_value( @pfx, 'circ_duration' );
57 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
58 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
59 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
61 $logger->error( "Missing circ script(s)" )
62 unless( $p and $c and $d and $f and $m and $pr );
64 $scripts{circ_permit_patron} = $p;
65 $scripts{circ_permit_copy} = $c;
66 $scripts{circ_duration} = $d;
67 $scripts{circ_recurring_fines} = $f;
68 $scripts{circ_max_fines} = $m;
69 $scripts{circ_permit_renew} = $pr;
72 "circulator: Loaded rules scripts for circ: " .
73 "circ permit patron = $p, ".
74 "circ permit copy = $c, ".
75 "circ duration = $d, ".
76 "circ recurring fines = $f, " .
77 "circ max fines = $m, ".
78 "circ renew permit = $pr. ".
80 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
84 __PACKAGE__->register_method(
85 method => "run_method",
86 api_name => "open-ils.circ.checkout.permit",
88 Determines if the given checkout can occur
89 @param authtoken The login session key
90 @param params A trailing hash of named params including
91 barcode : The copy barcode,
92 patron : The patron the checkout is occurring for,
93 renew : true or false - whether or not this is a renewal
94 @return The event that occurred during the permit check.
98 __PACKAGE__->register_method (
99 method => 'run_method',
100 api_name => 'open-ils.circ.checkout.permit.override',
101 signature => q/@see open-ils.circ.checkout.permit/,
105 __PACKAGE__->register_method(
106 method => "run_method",
107 api_name => "open-ils.circ.checkout",
110 @param authtoken The login session key
111 @param params A named hash of params including:
113 barcode If no copy is provided, the copy is retrieved via barcode
114 copyid If no copy or barcode is provide, the copy id will be use
115 patron The patron's id
116 noncat True if this is a circulation for a non-cataloted item
117 noncat_type The non-cataloged type id
118 noncat_circ_lib The location for the noncat circ.
119 precat The item has yet to be cataloged
120 dummy_title The temporary title of the pre-cataloded item
121 dummy_author The temporary authr of the pre-cataloded item
122 Default is the home org of the staff member
123 @return The SUCCESS event on success, any other event depending on the error
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.checkin",
131 Generic super-method for handling all copies
132 @param authtoken The login session key
133 @param params Hash of named parameters including:
134 barcode - The copy barcode
135 force - If true, copies in bad statuses will be checked in and give good statuses
136 noop - don't capture holds or put items into transit
137 void_overdues - void all overdues for the circulation (aka amnesty)
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkin.override",
145 signature => q/@see open-ils.circ.checkin/
148 __PACKAGE__->register_method(
149 method => "run_method",
150 api_name => "open-ils.circ.renew.override",
151 signature => q/@see open-ils.circ.renew/,
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.renew",
158 notes => <<" NOTES");
159 PARAMS( authtoken, circ => circ_id );
160 open-ils.circ.renew(login_session, circ_object);
161 Renews the provided circulation. login_session is the requestor of the
162 renewal and if the logged in user is not the same as circ->usr, then
163 the logged in user must have RENEW_CIRC permissions.
166 __PACKAGE__->register_method(
167 method => "run_method",
168 api_name => "open-ils.circ.checkout.full"
170 __PACKAGE__->register_method(
171 method => "run_method",
172 api_name => "open-ils.circ.checkout.full.override"
174 __PACKAGE__->register_method(
175 method => "run_method",
176 api_name => "open-ils.circ.reservation.pickup"
178 __PACKAGE__->register_method(
179 method => "run_method",
180 api_name => "open-ils.circ.reservation.return"
182 __PACKAGE__->register_method(
183 method => "run_method",
184 api_name => "open-ils.circ.reservation.return.override"
186 __PACKAGE__->register_method(
187 method => "run_method",
188 api_name => "open-ils.circ.checkout.inspect",
189 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
194 my( $self, $conn, $auth, $args ) = @_;
195 translate_legacy_args($args);
196 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
197 my $api = $self->api_name;
200 OpenILS::Application::Circ::Circulator->new($auth, %$args);
202 return circ_events($circulator) if $circulator->bail_out;
204 $circulator->use_booking(determine_booking_status());
206 # --------------------------------------------------------------------------
207 # First, check for a booking transit, as the barcode may not be a copy
208 # barcode, but a resource barcode, and nothing else in here will work
209 # --------------------------------------------------------------------------
211 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
212 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
213 if (@$resources) { # yes!
215 my $res_id_list = [ map { $_->id } @$resources ];
216 my $transit = $circulator->editor->search_action_reservation_transit_copy(
218 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
219 { order_by => { artc => 'source_send_time' }, limit => 1 }
221 )->[0]; # Any transit for this barcode?
223 if ($transit) { # yes! unwrap it.
225 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
226 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
228 my $success_event = new OpenILS::Event(
229 "SUCCESS", "payload" => {"reservation" => $reservation}
231 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
232 if (my $copy = $circulator->editor->search_asset_copy([
233 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
234 ])->[0]) { # got a copy
235 $copy->status( $transit->copy_status );
236 $copy->editor($circulator->editor->requestor->id);
237 $copy->edit_date('now');
238 $circulator->editor->update_asset_copy($copy);
239 $success_event->{"payload"}->{"record"} =
240 $U->record_to_mvr($copy->call_number->record);
241 $success_event->{"payload"}->{"volume"} = $copy->call_number;
242 $copy->call_number($copy->call_number->id);
243 $success_event->{"payload"}->{"copy"} = $copy;
247 $transit->dest_recv_time('now');
248 $circulator->editor->update_action_reservation_transit_copy( $transit );
250 $circulator->editor->commit;
251 # Formerly this branch just stopped here. Argh!
252 $conn->respond_complete($success_event);
260 # --------------------------------------------------------------------------
261 # Go ahead and load the script runner to make sure we have all
262 # of the objects we need
263 # --------------------------------------------------------------------------
265 if ($circulator->use_booking) {
266 $circulator->is_res_checkin($circulator->is_checkin(1))
267 if $api =~ /reservation.return/ or (
268 $api =~ /checkin/ and $circulator->seems_like_reservation()
271 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
274 $circulator->is_renewal(1) if $api =~ /renew/;
275 $circulator->is_checkin(1) if $api =~ /checkin/;
277 $circulator->mk_env();
278 $circulator->noop(1) if $circulator->claims_never_checked_out;
280 if($legacy_script_support and not $circulator->is_checkin) {
281 $circulator->mk_script_runner();
282 $circulator->legacy_script_support(1);
283 $circulator->circ_permit_patron($scripts{circ_permit_patron});
284 $circulator->circ_permit_copy($scripts{circ_permit_copy});
285 $circulator->circ_duration($scripts{circ_duration});
286 $circulator->circ_permit_renew($scripts{circ_permit_renew});
288 return circ_events($circulator) if $circulator->bail_out;
291 $circulator->override(1) if $api =~ /override/o;
293 if( $api =~ /checkout\.permit/ ) {
294 $circulator->do_permit();
296 } elsif( $api =~ /checkout.full/ ) {
298 # requesting a precat checkout implies that any required
299 # overrides have been performed. Go ahead and re-override.
300 $circulator->skip_permit_key(1);
301 $circulator->override(1) if $circulator->request_precat;
302 $circulator->do_permit();
303 $circulator->is_checkout(1);
304 unless( $circulator->bail_out ) {
305 $circulator->events([]);
306 $circulator->do_checkout();
309 } elsif( $circulator->is_res_checkout ) {
310 $circulator->do_reservation_pickup();
312 } elsif( $api =~ /inspect/ ) {
313 my $data = $circulator->do_inspect();
314 $circulator->editor->rollback;
317 } elsif( $api =~ /checkout/ ) {
318 $circulator->is_checkout(1);
319 $circulator->do_checkout();
321 } elsif( $circulator->is_res_checkin ) {
322 $circulator->do_reservation_return();
323 $circulator->do_checkin() if ($circulator->copy());
324 } elsif( $api =~ /checkin/ ) {
325 $circulator->do_checkin();
327 } elsif( $api =~ /renew/ ) {
328 $circulator->is_renewal(1);
329 $circulator->do_renew();
332 if( $circulator->bail_out ) {
335 # make sure no success event accidentally slip in
337 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
340 my @e = @{$circulator->events};
341 push( @ee, $_->{textcode} ) for @e;
342 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
344 $circulator->editor->rollback;
348 $circulator->editor->commit;
350 if ($circulator->generate_lost_overdue) {
351 # Generating additional overdue billings has to happen after the
352 # main commit and before the final respond() so the caller can
353 # receive the latest transaction summary.
354 my $evt = $circulator->generate_lost_overdue_fines;
355 $circulator->bail_on_events($evt) if $evt;
359 $conn->respond_complete(circ_events($circulator));
361 $circulator->script_runner->cleanup if $circulator->script_runner;
363 return undef if $circulator->bail_out;
365 $circulator->do_hold_notify($circulator->notify_hold)
366 if $circulator->notify_hold;
367 $circulator->retarget_holds if $circulator->retarget;
368 $circulator->append_reading_list;
369 $circulator->make_trigger_events;
376 my @e = @{$circ->events};
377 # if we have multiple events, SUCCESS should not be one of them;
378 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
379 return (@e == 1) ? $e[0] : \@e;
383 sub translate_legacy_args {
386 if( $$args{barcode} ) {
387 $$args{copy_barcode} = $$args{barcode};
388 delete $$args{barcode};
391 if( $$args{copyid} ) {
392 $$args{copy_id} = $$args{copyid};
393 delete $$args{copyid};
396 if( $$args{patronid} ) {
397 $$args{patron_id} = $$args{patronid};
398 delete $$args{patronid};
401 if( $$args{patron} and !ref($$args{patron}) ) {
402 $$args{patron_id} = $$args{patron};
403 delete $$args{patron};
407 if( $$args{noncat} ) {
408 $$args{is_noncat} = $$args{noncat};
409 delete $$args{noncat};
412 if( $$args{precat} ) {
413 $$args{is_precat} = $$args{request_precat} = $$args{precat};
414 delete $$args{precat};
420 # --------------------------------------------------------------------------
421 # This package actually manages all of the circulation logic
422 # --------------------------------------------------------------------------
423 package OpenILS::Application::Circ::Circulator;
424 use strict; use warnings;
425 use vars q/$AUTOLOAD/;
427 use OpenILS::Utils::Fieldmapper;
428 use OpenSRF::Utils::Cache;
429 use Digest::MD5 qw(md5_hex);
430 use DateTime::Format::ISO8601;
431 use OpenILS::Utils::PermitHold;
432 use OpenSRF::Utils qw/:datetime/;
433 use OpenSRF::Utils::SettingsClient;
434 use OpenILS::Application::Circ::Holds;
435 use OpenILS::Application::Circ::Transit;
436 use OpenSRF::Utils::Logger qw(:logger);
437 use OpenILS::Utils::CStoreEditor qw/:funcs/;
438 use OpenILS::Application::Circ::ScriptBuilder;
439 use OpenILS::Const qw/:const/;
440 use OpenILS::Utils::Penalty;
441 use OpenILS::Application::Circ::CircCommon;
444 my $holdcode = "OpenILS::Application::Circ::Holds";
445 my $transcode = "OpenILS::Application::Circ::Transit";
451 # --------------------------------------------------------------------------
452 # Add a pile of automagic getter/setter methods
453 # --------------------------------------------------------------------------
454 my @AUTOLOAD_FIELDS = qw/
501 recurring_fines_level
514 cancelled_hold_transit
521 circ_matrix_matchpoint
523 legacy_script_support
533 claims_never_checked_out
538 generate_lost_overdue
551 my $type = ref($self) or die "$self is not an object";
553 my $name = $AUTOLOAD;
556 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
557 $logger->error("circulator: $type: invalid autoload field: $name");
558 die "$type: invalid autoload field: $name\n"
563 *{"${type}::${name}"} = sub {
566 $s->{$name} = $v if defined $v;
570 return $self->$name($data);
575 my( $class, $auth, %args ) = @_;
576 $class = ref($class) || $class;
577 my $self = bless( {}, $class );
580 $self->editor(new_editor(xact => 1, authtoken => $auth));
582 unless( $self->editor->checkauth ) {
583 $self->bail_on_events($self->editor->event);
587 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
589 $self->$_($args{$_}) for keys %args;
592 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
594 # if this is a renewal, default to desk_renewal
595 $self->desk_renewal(1) unless
596 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
598 $self->capture('') unless $self->capture;
600 unless(%user_groups) {
601 my $gps = $self->editor->retrieve_all_permission_grp_tree;
602 %user_groups = map { $_->id => $_ } @$gps;
609 # --------------------------------------------------------------------------
610 # True if we should discontinue processing
611 # --------------------------------------------------------------------------
613 my( $self, $bool ) = @_;
614 if( defined $bool ) {
615 $logger->info("circulator: BAILING OUT") if $bool;
616 $self->{bail_out} = $bool;
618 return $self->{bail_out};
623 my( $self, @evts ) = @_;
626 $e->{payload} = $self->copy if
627 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
629 $logger->info("circulator: pushing event ".$e->{textcode});
630 push( @{$self->events}, $e ) unless
631 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
637 return '' if $self->skip_permit_key;
638 my $key = md5_hex( time() . rand() . "$$" );
639 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
640 return $self->permit_key($key);
643 sub check_permit_key {
645 return 1 if $self->skip_permit_key;
646 my $key = $self->permit_key;
647 return 0 unless $key;
648 my $k = "oils_permit_key_$key";
649 my $one = $self->cache_handle->get_cache($k);
650 $self->cache_handle->delete_cache($k);
651 return ($one) ? 1 : 0;
654 sub seems_like_reservation {
657 # Some words about the following method:
658 # 1) It requires the VIEW_USER permission, but that's not an
659 # issue, right, since all staff should have that?
660 # 2) It returns only one reservation at a time, even if an item can be
661 # and is currently overbooked. Hmmm....
662 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
663 my $result = $booking_ses->request(
664 "open-ils.booking.reservations.by_returnable_resource_barcode",
665 $self->editor->authtoken,
668 $booking_ses->disconnect;
670 return $self->bail_on_events($result) if defined $U->event_code($result);
673 $self->reservation(shift @$result);
681 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
682 sub save_trimmed_copy {
683 my ($self, $copy) = @_;
686 $self->volume($copy->call_number);
687 $self->title($self->volume->record);
688 $self->copy->call_number($self->volume->id);
689 $self->volume->record($self->title->id);
690 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
691 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
692 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
693 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
699 my $e = $self->editor;
701 # --------------------------------------------------------------------------
702 # Grab the fleshed copy
703 # --------------------------------------------------------------------------
704 unless($self->is_noncat) {
707 $copy = $e->retrieve_asset_copy(
708 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
710 } elsif( $self->copy_barcode ) {
712 $copy = $e->search_asset_copy(
713 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
714 } elsif( $self->reservation ) {
715 my $res = $e->json_query(
717 "select" => {"acp" => ["id"]},
722 "field" => "barcode",
726 "field" => "current_resource"
734 "id" => (ref $self->reservation) ?
735 $self->reservation->id : $self->reservation
740 if (ref $res eq "ARRAY" and scalar @$res) {
741 $logger->info("circulator: mapped reservation " .
742 $self->reservation . " to copy " . $res->[0]->{"id"});
743 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
748 $self->save_trimmed_copy($copy);
750 # We can't renew if there is no copy
751 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
752 if $self->is_renewal;
757 # --------------------------------------------------------------------------
759 # --------------------------------------------------------------------------
763 flesh_fields => {au => [ qw/ card / ]}
766 if( $self->patron_id ) {
767 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
768 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
770 } elsif( $self->patron_barcode ) {
772 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
773 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
774 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
776 $patron = $e->retrieve_actor_user($card->usr)
777 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
779 # Use the card we looked up, not the patron's primary, for card active checks
780 $patron->card($card);
783 if( my $copy = $self->copy ) {
786 $flesh->{flesh_fields}->{circ} = ['usr'];
788 my $circ = $e->search_action_circulation([
789 {target_copy => $copy->id, checkin_time => undef}, $flesh
793 $patron = $circ->usr;
794 $circ->usr($patron->id); # de-flesh for consistency
800 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
801 unless $self->patron($patron) or $self->is_checkin;
803 unless($self->is_checkin) {
805 # Check for inactivity and patron reg. expiration
807 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
808 unless $U->is_true($patron->active);
810 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
811 unless $U->is_true($patron->card->active);
813 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
814 cleanse_ISO8601($patron->expire_date));
816 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
817 if( CORE::time > $expire->epoch ) ;
821 # --------------------------------------------------------------------------
822 # This builds the script runner environment and fetches most of the
824 # --------------------------------------------------------------------------
825 sub mk_script_runner {
831 qw/copy copy_barcode copy_id patron
832 patron_id patron_barcode volume title editor/;
834 # Translate our objects into the ScriptBuilder args hash
835 $$args{$_} = $self->$_() for @fields;
837 $args->{ignore_user_status} = 1 if $self->is_checkin;
838 $$args{fetch_patron_by_circ_copy} = 1;
839 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
841 if( my $pco = $self->pending_checkouts ) {
842 $logger->info("circulator: we were given a pending checkouts number of $pco");
843 $$args{patronItemsOut} = $pco;
846 # This fetches most of the objects we need
847 $self->script_runner(
848 OpenILS::Application::Circ::ScriptBuilder->build($args));
850 # Now we translate the ScriptBuilder objects back into self
851 $self->$_($$args{$_}) for @fields;
853 my @evts = @{$args->{_events}} if $args->{_events};
855 $logger->debug("circulator: script builder returned events: @evts") if @evts;
859 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
860 if(!$self->is_noncat and
862 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
866 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
867 return $self->bail_on_events(@e);
872 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
873 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
874 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
875 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
879 # We can't renew if there is no copy
880 return $self->bail_on_events(@evts) if
881 $self->is_renewal and !$self->copy;
883 # Set some circ-specific flags in the script environment
884 my $evt = "environment";
885 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
887 if( $self->is_noncat ) {
888 $self->script_runner->insert("$evt.isNonCat", 1);
889 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
892 if( $self->is_precat ) {
893 $self->script_runner->insert("environment.isPrecat", 1, 1);
896 $self->script_runner->add_path( $_ ) for @$script_libs;
901 # --------------------------------------------------------------------------
902 # Does the circ permit work
903 # --------------------------------------------------------------------------
907 $self->log_me("do_permit()");
909 unless( $self->editor->requestor->id == $self->patron->id ) {
910 return $self->bail_on_events($self->editor->event)
911 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
914 $self->check_captured_holds();
915 $self->do_copy_checks();
916 return if $self->bail_out;
917 $self->run_patron_permit_scripts();
918 $self->run_copy_permit_scripts()
919 unless $self->is_precat or $self->is_noncat;
920 $self->check_item_deposit_events();
921 $self->override_events();
922 return if $self->bail_out;
924 if($self->is_precat and not $self->request_precat) {
927 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
928 return $self->bail_out(1) unless $self->is_renewal;
932 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
935 sub check_item_deposit_events {
937 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
938 if $self->is_deposit and not $self->is_deposit_exempt;
939 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
940 if $self->is_rental and not $self->is_rental_exempt;
943 # returns true if the user is not required to pay deposits
944 sub is_deposit_exempt {
946 my $pid = (ref $self->patron->profile) ?
947 $self->patron->profile->id : $self->patron->profile;
948 my $groups = $U->ou_ancestor_setting_value(
949 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
950 for my $grp (@$groups) {
951 return 1 if $self->is_group_descendant($grp, $pid);
956 # returns true if the user is not required to pay rental fees
957 sub is_rental_exempt {
959 my $pid = (ref $self->patron->profile) ?
960 $self->patron->profile->id : $self->patron->profile;
961 my $groups = $U->ou_ancestor_setting_value(
962 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
963 for my $grp (@$groups) {
964 return 1 if $self->is_group_descendant($grp, $pid);
969 sub is_group_descendant {
970 my($self, $p_id, $c_id) = @_;
971 return 0 unless defined $p_id and defined $c_id;
972 return 1 if $c_id == $p_id;
973 while(my $grp = $user_groups{$c_id}) {
974 $c_id = $grp->parent;
975 return 0 unless defined $c_id;
976 return 1 if $c_id == $p_id;
981 sub check_captured_holds {
983 my $copy = $self->copy;
984 my $patron = $self->patron;
986 return undef unless $copy;
988 my $s = $U->copy_status($copy->status)->id;
989 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
990 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
992 # Item is on the holds shelf, make sure it's going to the right person
993 my $hold = $self->editor->search_action_hold_request(
996 current_copy => $copy->id ,
997 capture_time => { '!=' => undef },
998 cancel_time => undef,
999 fulfillment_time => undef
1005 if ($hold and $hold->usr == $patron->id) {
1006 $self->checkout_is_for_hold(1);
1010 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1012 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1016 sub do_copy_checks {
1018 my $copy = $self->copy;
1019 return unless $copy;
1021 my $stat = $U->copy_status($copy->status)->id;
1023 # We cannot check out a copy if it is in-transit
1024 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1025 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1028 $self->handle_claims_returned();
1029 return if $self->bail_out;
1031 # no claims returned circ was found, check if there is any open circ
1032 unless( $self->is_renewal ) {
1034 my $circs = $self->editor->search_action_circulation(
1035 { target_copy => $copy->id, checkin_time => undef }
1038 if(my $old_circ = $circs->[0]) { # an open circ was found
1040 my $payload = {copy => $copy};
1042 if($old_circ->usr == $self->patron->id) {
1044 $payload->{old_circ} = $old_circ;
1046 # If there is an open circulation on the checkout item and an auto-renew
1047 # interval is defined, inform the caller that they should go
1048 # ahead and renew the item instead of warning about open circulations.
1050 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1052 'circ.checkout_auto_renew_age',
1056 if($auto_renew_intvl) {
1057 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1058 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1060 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1061 $payload->{auto_renew} = 1;
1066 return $self->bail_on_events(
1067 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1073 my $LEGACY_CIRC_EVENT_MAP = {
1074 'no_item' => 'ITEM_NOT_CATALOGED',
1075 'actor.usr.barred' => 'PATRON_BARRED',
1076 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1077 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1078 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1079 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1080 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1081 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1082 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1083 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1087 # ---------------------------------------------------------------------
1088 # This pushes any patron-related events into the list but does not
1089 # set bail_out for any events
1090 # ---------------------------------------------------------------------
1091 sub run_patron_permit_scripts {
1093 my $runner = $self->script_runner;
1094 my $patronid = $self->patron->id;
1098 if(!$self->legacy_script_support) {
1100 my $results = $self->run_indb_circ_test;
1101 unless($self->circ_test_success) {
1102 my @trimmed_results;
1104 if ($self->is_noncat) {
1105 # no_item result is OK during noncat checkout
1106 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1108 @trimmed_results = @$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 or
1122 ($self->is_noncat and $name eq 'no_item');
1123 push(@trimmed_results, $res);
1127 # update the final set of test results
1128 $self->matrix_test_result(\@trimmed_results);
1130 push @allevents, $self->matrix_test_result_events;
1135 # ---------------------------------------------------------------------
1136 # # Now run the patron permit script
1137 # ---------------------------------------------------------------------
1138 $runner->load($self->circ_permit_patron);
1139 my $result = $runner->run or
1140 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1142 my $patron_events = $result->{events};
1144 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1145 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1146 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1147 $penalties = $penalties->{fatal_penalties};
1149 for my $pen (@$penalties) {
1150 # CIRC blocks are ignored if this is a FULFILL scenario
1151 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1152 my $event = OpenILS::Event->new($pen->name);
1153 $event->{desc} = $pen->label;
1154 push(@allevents, $event);
1157 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1161 $_->{payload} = $self->copy if
1162 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1165 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1167 $self->push_events(@allevents);
1170 sub matrix_test_result_codes {
1172 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1175 sub matrix_test_result_events {
1178 my $event = new OpenILS::Event(
1179 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1181 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1183 } (@{$self->matrix_test_result});
1186 sub run_indb_circ_test {
1188 return $self->matrix_test_result if $self->matrix_test_result;
1190 my $dbfunc = ($self->is_renewal) ?
1191 'action.item_user_renew_test' : 'action.item_user_circ_test';
1193 if( $self->is_precat && $self->request_precat) {
1194 $self->make_precat_copy;
1195 return if $self->bail_out;
1198 my $results = $self->editor->json_query(
1202 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1208 $self->circ_test_success($U->is_true($results->[0]->{success}));
1210 if(my $mp = $results->[0]->{matchpoint}) {
1211 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1212 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1213 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1214 if(defined($results->[0]->{renewals})) {
1215 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1217 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1218 if(defined($results->[0]->{grace_period})) {
1219 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1221 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1222 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1223 # Grab the *last* response for limit_groups, where it is more likely to be filled
1224 $self->limit_groups($results->[-1]->{limit_groups});
1227 return $self->matrix_test_result($results);
1230 # ---------------------------------------------------------------------
1231 # given a use and copy, this will calculate the circulation policy
1232 # parameters. Only works with in-db circ.
1233 # ---------------------------------------------------------------------
1237 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1239 $self->run_indb_circ_test;
1242 circ_test_success => $self->circ_test_success,
1243 failure_events => [],
1244 failure_codes => [],
1245 matchpoint => $self->circ_matrix_matchpoint
1248 unless($self->circ_test_success) {
1249 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1250 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1253 if($self->circ_matrix_matchpoint) {
1254 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1255 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1256 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1257 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1259 my $policy = $self->get_circ_policy(
1260 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1262 $$results{$_} = $$policy{$_} for keys %$policy;
1268 # ---------------------------------------------------------------------
1269 # Loads the circ policy info for duration, recurring fine, and max
1270 # fine based on the current copy
1271 # ---------------------------------------------------------------------
1272 sub get_circ_policy {
1273 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1276 duration_rule => $duration_rule->name,
1277 recurring_fine_rule => $recurring_fine_rule->name,
1278 max_fine_rule => $max_fine_rule->name,
1279 max_fine => $self->get_max_fine_amount($max_fine_rule),
1280 fine_interval => $recurring_fine_rule->recurrence_interval,
1281 renewal_remaining => $duration_rule->max_renewals,
1282 grace_period => $recurring_fine_rule->grace_period
1285 if($hard_due_date) {
1286 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1287 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1290 $policy->{duration_date_ceiling} = undef;
1291 $policy->{duration_date_ceiling_force} = undef;
1294 $policy->{duration} = $duration_rule->shrt
1295 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1296 $policy->{duration} = $duration_rule->normal
1297 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1298 $policy->{duration} = $duration_rule->extended
1299 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1301 $policy->{recurring_fine} = $recurring_fine_rule->low
1302 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1303 $policy->{recurring_fine} = $recurring_fine_rule->normal
1304 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1305 $policy->{recurring_fine} = $recurring_fine_rule->high
1306 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1311 sub get_max_fine_amount {
1313 my $max_fine_rule = shift;
1314 my $max_amount = $max_fine_rule->amount;
1316 # if is_percent is true then the max->amount is
1317 # use as a percentage of the copy price
1318 if ($U->is_true($max_fine_rule->is_percent)) {
1319 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1320 $max_amount = $price * $max_fine_rule->amount / 100;
1322 $U->ou_ancestor_setting_value(
1324 'circ.max_fine.cap_at_price',
1328 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1329 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1337 sub run_copy_permit_scripts {
1339 my $copy = $self->copy || return;
1340 my $runner = $self->script_runner;
1344 if(!$self->legacy_script_support) {
1345 my $results = $self->run_indb_circ_test;
1346 push @allevents, $self->matrix_test_result_events
1347 unless $self->circ_test_success;
1350 # ---------------------------------------------------------------------
1351 # Capture all of the copy permit events
1352 # ---------------------------------------------------------------------
1353 $runner->load($self->circ_permit_copy);
1354 my $result = $runner->run or
1355 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1356 my $copy_events = $result->{events};
1358 # ---------------------------------------------------------------------
1359 # Now collect all of the events together
1360 # ---------------------------------------------------------------------
1361 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1364 # See if this copy has an alert message
1365 my $ae = $self->check_copy_alert();
1366 push( @allevents, $ae ) if $ae;
1368 # uniquify the events
1369 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1370 @allevents = values %hash;
1372 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1374 $self->push_events(@allevents);
1378 sub check_copy_alert {
1380 return undef if $self->is_renewal;
1381 return OpenILS::Event->new(
1382 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1383 if $self->copy and $self->copy->alert_message;
1389 # --------------------------------------------------------------------------
1390 # If the call is overriding and has permissions to override every collected
1391 # event, the are cleared. Any event that the caller does not have
1392 # permission to override, will be left in the event list and bail_out will
1394 # XXX We need code in here to cancel any holds/transits on copies
1395 # that are being force-checked out
1396 # --------------------------------------------------------------------------
1397 sub override_events {
1399 my @events = @{$self->events};
1400 return unless @events;
1401 my $oargs = $self->override_args;
1403 if(!$self->override) {
1404 return $self->bail_out(1)
1405 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1410 for my $e (@events) {
1411 my $tc = $e->{textcode};
1412 next if $tc eq 'SUCCESS';
1413 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1414 my $ov = "$tc.override";
1415 $logger->info("circulator: attempting to override event: $ov");
1417 return $self->bail_on_events($self->editor->event)
1418 unless( $self->editor->allowed($ov) );
1420 return $self->bail_out(1);
1426 # --------------------------------------------------------------------------
1427 # If there is an open claimsreturn circ on the requested copy, close the
1428 # circ if overriding, otherwise bail out
1429 # --------------------------------------------------------------------------
1430 sub handle_claims_returned {
1432 my $copy = $self->copy;
1434 my $CR = $self->editor->search_action_circulation(
1436 target_copy => $copy->id,
1437 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1438 checkin_time => undef,
1442 return unless ($CR = $CR->[0]);
1446 # - If the caller has set the override flag, we will check the item in
1447 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1449 $CR->checkin_time('now');
1450 $CR->checkin_scan_time('now');
1451 $CR->checkin_lib($self->circ_lib);
1452 $CR->checkin_workstation($self->editor->requestor->wsid);
1453 $CR->checkin_staff($self->editor->requestor->id);
1455 $evt = $self->editor->event
1456 unless $self->editor->update_action_circulation($CR);
1459 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1462 $self->bail_on_events($evt) if $evt;
1467 # --------------------------------------------------------------------------
1468 # This performs the checkout
1469 # --------------------------------------------------------------------------
1473 $self->log_me("do_checkout()");
1475 # make sure perms are good if this isn't a renewal
1476 unless( $self->is_renewal ) {
1477 return $self->bail_on_events($self->editor->event)
1478 unless( $self->editor->allowed('COPY_CHECKOUT') );
1481 # verify the permit key
1482 unless( $self->check_permit_key ) {
1483 if( $self->permit_override ) {
1484 return $self->bail_on_events($self->editor->event)
1485 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1487 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1491 # if this is a non-cataloged circ, build the circ and finish
1492 if( $self->is_noncat ) {
1493 $self->checkout_noncat;
1495 OpenILS::Event->new('SUCCESS',
1496 payload => { noncat_circ => $self->circ }));
1500 if( $self->is_precat ) {
1501 $self->make_precat_copy;
1502 return if $self->bail_out;
1504 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1505 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1508 $self->do_copy_checks;
1509 return if $self->bail_out;
1511 $self->run_checkout_scripts();
1512 return if $self->bail_out;
1514 $self->build_checkout_circ_object();
1515 return if $self->bail_out;
1517 my $modify_to_start = $self->booking_adjusted_due_date();
1518 return if $self->bail_out;
1520 $self->apply_modified_due_date($modify_to_start);
1521 return if $self->bail_out;
1523 return $self->bail_on_events($self->editor->event)
1524 unless $self->editor->create_action_circulation($self->circ);
1526 # refresh the circ to force local time zone for now
1527 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1529 if($self->limit_groups) {
1530 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1533 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1535 return if $self->bail_out;
1537 $self->apply_deposit_fee();
1538 return if $self->bail_out;
1540 $self->handle_checkout_holds();
1541 return if $self->bail_out;
1543 # ------------------------------------------------------------------------------
1544 # Update the patron penalty info in the DB. Run it for permit-overrides
1545 # since the penalties are not updated during the permit phase
1546 # ------------------------------------------------------------------------------
1547 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1549 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1552 if($self->is_renewal) {
1553 # flesh the billing summary for the checked-in circ
1554 $pcirc = $self->editor->retrieve_action_circulation([
1556 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1561 OpenILS::Event->new('SUCCESS',
1563 copy => $U->unflesh_copy($self->copy),
1564 volume => $self->volume,
1565 circ => $self->circ,
1567 holds_fulfilled => $self->fulfilled_holds,
1568 deposit_billing => $self->deposit_billing,
1569 rental_billing => $self->rental_billing,
1570 parent_circ => $pcirc,
1571 patron => ($self->return_patron) ? $self->patron : undef,
1572 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1578 sub apply_deposit_fee {
1580 my $copy = $self->copy;
1582 ($self->is_deposit and not $self->is_deposit_exempt) or
1583 ($self->is_rental and not $self->is_rental_exempt);
1585 return if $self->is_deposit and $self->skip_deposit_fee;
1586 return if $self->is_rental and $self->skip_rental_fee;
1588 my $bill = Fieldmapper::money::billing->new;
1589 my $amount = $copy->deposit_amount;
1593 if($self->is_deposit) {
1594 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1596 $self->deposit_billing($bill);
1598 $billing_type = OILS_BILLING_TYPE_RENTAL;
1600 $self->rental_billing($bill);
1603 $bill->xact($self->circ->id);
1604 $bill->amount($amount);
1605 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1606 $bill->billing_type($billing_type);
1607 $bill->btype($btype);
1608 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1610 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1615 my $copy = $self->copy;
1617 my $stat = $copy->status if ref $copy->status;
1618 my $loc = $copy->location if ref $copy->location;
1619 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1621 $copy->status($stat->id) if $stat;
1622 $copy->location($loc->id) if $loc;
1623 $copy->circ_lib($circ_lib->id) if $circ_lib;
1624 $copy->editor($self->editor->requestor->id);
1625 $copy->edit_date('now');
1626 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1628 return $self->bail_on_events($self->editor->event)
1629 unless $self->editor->update_asset_copy($self->copy);
1631 $copy->status($U->copy_status($copy->status));
1632 $copy->location($loc) if $loc;
1633 $copy->circ_lib($circ_lib) if $circ_lib;
1636 sub update_reservation {
1638 my $reservation = $self->reservation;
1640 my $usr = $reservation->usr;
1641 my $target_rt = $reservation->target_resource_type;
1642 my $target_r = $reservation->target_resource;
1643 my $current_r = $reservation->current_resource;
1645 $reservation->usr($usr->id) if ref $usr;
1646 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1647 $reservation->target_resource($target_r->id) if ref $target_r;
1648 $reservation->current_resource($current_r->id) if ref $current_r;
1650 return $self->bail_on_events($self->editor->event)
1651 unless $self->editor->update_booking_reservation($self->reservation);
1654 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1655 $self->reservation($reservation);
1659 sub bail_on_events {
1660 my( $self, @evts ) = @_;
1661 $self->push_events(@evts);
1665 # ------------------------------------------------------------------------------
1666 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1667 # affects copies that will fulfill holds and CIRC affects all other copies.
1668 # If blocks exists, bail, push Events onto the event pile, and return true.
1669 # ------------------------------------------------------------------------------
1670 sub check_hold_fulfill_blocks {
1673 # See if the user has any penalties applied that prevent hold fulfillment
1674 my $pens = $self->editor->json_query({
1675 select => {csp => ['name', 'label']},
1676 from => {ausp => {csp => {}}},
1679 usr => $self->patron->id,
1680 org_unit => $U->get_org_full_path($self->circ_lib),
1682 {stop_date => undef},
1683 {stop_date => {'>' => 'now'}}
1686 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1690 return 0 unless @$pens;
1692 for my $pen (@$pens) {
1693 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1694 my $event = OpenILS::Event->new($pen->{name});
1695 $event->{desc} = $pen->{label};
1696 $self->push_events($event);
1699 $self->override_events;
1700 return $self->bail_out;
1704 # ------------------------------------------------------------------------------
1705 # When an item is checked out, see if we can fulfill a hold for this patron
1706 # ------------------------------------------------------------------------------
1707 sub handle_checkout_holds {
1709 my $copy = $self->copy;
1710 my $patron = $self->patron;
1712 my $e = $self->editor;
1713 $self->fulfilled_holds([]);
1715 # pre/non-cats can't fulfill a hold
1716 return if $self->is_precat or $self->is_noncat;
1718 my $hold = $e->search_action_hold_request({
1719 current_copy => $copy->id ,
1720 cancel_time => undef,
1721 fulfillment_time => undef,
1723 {expire_time => undef},
1724 {expire_time => {'>' => 'now'}}
1728 if($hold and $hold->usr != $patron->id) {
1729 # reset the hold since the copy is now checked out
1731 $logger->info("circulator: un-targeting hold ".$hold->id.
1732 " because copy ".$copy->id." is getting checked out");
1734 $hold->clear_prev_check_time;
1735 $hold->clear_current_copy;
1736 $hold->clear_capture_time;
1737 $hold->clear_shelf_time;
1738 $hold->clear_shelf_expire_time;
1739 $hold->clear_current_shelf_lib;
1741 return $self->bail_on_event($e->event)
1742 unless $e->update_action_hold_request($hold);
1748 $hold = $self->find_related_user_hold($copy, $patron) or return;
1749 $logger->info("circulator: found related hold to fulfill in checkout");
1752 return if $self->check_hold_fulfill_blocks;
1754 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1756 # if the hold was never officially captured, capture it.
1757 $hold->current_copy($copy->id);
1758 $hold->capture_time('now') unless $hold->capture_time;
1759 $hold->fulfillment_time('now');
1760 $hold->fulfillment_staff($e->requestor->id);
1761 $hold->fulfillment_lib($self->circ_lib);
1763 return $self->bail_on_events($e->event)
1764 unless $e->update_action_hold_request($hold);
1766 $holdcode->delete_hold_copy_maps($e, $hold->id);
1767 return $self->fulfilled_holds([$hold->id]);
1771 # ------------------------------------------------------------------------------
1772 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1773 # the patron directly targets the checked out item, see if there is another hold
1774 # for the patron that could be fulfilled by the checked out item. Fulfill the
1775 # oldest hold and only fulfill 1 of them.
1777 # For "another hold":
1779 # First, check for one that the copy matches via hold_copy_map, ensuring that
1780 # *any* hold type that this copy could fill may end up filled.
1782 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1783 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1784 # that are non-requestable to count as capturing those hold types.
1785 # ------------------------------------------------------------------------------
1786 sub find_related_user_hold {
1787 my($self, $copy, $patron) = @_;
1788 my $e = $self->editor;
1790 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1792 return undef unless $U->ou_ancestor_setting_value(
1793 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1795 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1797 select => {ahr => ['id']},
1806 fkey => 'current_copy',
1807 type => 'left' # there may be no current_copy
1814 fulfillment_time => undef,
1815 cancel_time => undef,
1817 {expire_time => undef},
1818 {expire_time => {'>' => 'now'}}
1822 target_copy => $self->copy->id
1826 {id => undef}, # left-join copy may be nonexistent
1827 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1831 order_by => {ahr => {request_time => {direction => 'asc'}}},
1835 my $hold_info = $e->json_query($args)->[0];
1836 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1837 return undef if $U->ou_ancestor_setting_value(
1838 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1840 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1842 select => {ahr => ['id']},
1847 fkey => 'current_copy',
1848 type => 'left' # there may be no current_copy
1855 fulfillment_time => undef,
1856 cancel_time => undef,
1858 {expire_time => undef},
1859 {expire_time => {'>' => 'now'}}
1866 target => $self->volume->id
1872 target => $self->title->id
1878 {id => undef}, # left-join copy may be nonexistent
1879 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1883 order_by => {ahr => {request_time => {direction => 'asc'}}},
1887 $hold_info = $e->json_query($args)->[0];
1888 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1893 sub run_checkout_scripts {
1898 my $runner = $self->script_runner;
1907 my $hard_due_date_name;
1909 if(!$self->legacy_script_support) {
1910 $self->run_indb_circ_test();
1911 $duration = $self->circ_matrix_matchpoint->duration_rule;
1912 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1913 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1914 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1918 $runner->load($self->circ_duration);
1920 my $result = $runner->run or
1921 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1923 $duration_name = $result->{durationRule};
1924 $recurring_name = $result->{recurringFinesRule};
1925 $max_fine_name = $result->{maxFine};
1926 $hard_due_date_name = $result->{hardDueDate};
1929 $duration_name = $duration->name if $duration;
1930 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1933 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1934 return $self->bail_on_events($evt) if ($evt && !$nobail);
1936 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1937 return $self->bail_on_events($evt) if ($evt && !$nobail);
1939 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1940 return $self->bail_on_events($evt) if ($evt && !$nobail);
1942 if($hard_due_date_name) {
1943 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1944 return $self->bail_on_events($evt) if ($evt && !$nobail);
1950 # The item circulates with an unlimited duration
1954 $hard_due_date = undef;
1957 $self->duration_rule($duration);
1958 $self->recurring_fines_rule($recurring);
1959 $self->max_fine_rule($max_fine);
1960 $self->hard_due_date($hard_due_date);
1964 sub build_checkout_circ_object {
1967 my $circ = Fieldmapper::action::circulation->new;
1968 my $duration = $self->duration_rule;
1969 my $max = $self->max_fine_rule;
1970 my $recurring = $self->recurring_fines_rule;
1971 my $hard_due_date = $self->hard_due_date;
1972 my $copy = $self->copy;
1973 my $patron = $self->patron;
1974 my $duration_date_ceiling;
1975 my $duration_date_ceiling_force;
1979 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1980 $duration_date_ceiling = $policy->{duration_date_ceiling};
1981 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1983 my $dname = $duration->name;
1984 my $mname = $max->name;
1985 my $rname = $recurring->name;
1987 if($hard_due_date) {
1988 $hdname = $hard_due_date->name;
1991 $logger->debug("circulator: building circulation ".
1992 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1994 $circ->duration($policy->{duration});
1995 $circ->recurring_fine($policy->{recurring_fine});
1996 $circ->duration_rule($duration->name);
1997 $circ->recurring_fine_rule($recurring->name);
1998 $circ->max_fine_rule($max->name);
1999 $circ->max_fine($policy->{max_fine});
2000 $circ->fine_interval($recurring->recurrence_interval);
2001 $circ->renewal_remaining($duration->max_renewals);
2002 $circ->grace_period($policy->{grace_period});
2006 $logger->info("circulator: copy found with an unlimited circ duration");
2007 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2008 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2009 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2010 $circ->renewal_remaining(0);
2011 $circ->grace_period(0);
2014 $circ->target_copy( $copy->id );
2015 $circ->usr( $patron->id );
2016 $circ->circ_lib( $self->circ_lib );
2017 $circ->workstation($self->editor->requestor->wsid)
2018 if defined $self->editor->requestor->wsid;
2020 # renewals maintain a link to the parent circulation
2021 $circ->parent_circ($self->parent_circ);
2023 if( $self->is_renewal ) {
2024 $circ->opac_renewal('t') if $self->opac_renewal;
2025 $circ->phone_renewal('t') if $self->phone_renewal;
2026 $circ->desk_renewal('t') if $self->desk_renewal;
2027 $circ->renewal_remaining($self->renewal_remaining);
2028 $circ->circ_staff($self->editor->requestor->id);
2032 # if the user provided an overiding checkout time,
2033 # (e.g. the checkout really happened several hours ago), then
2034 # we apply that here. Does this need a perm??
2035 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2036 if $self->checkout_time;
2038 # if a patron is renewing, 'requestor' will be the patron
2039 $circ->circ_staff($self->editor->requestor->id);
2040 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
2045 sub do_reservation_pickup {
2048 $self->log_me("do_reservation_pickup()");
2050 $self->reservation->pickup_time('now');
2053 $self->reservation->current_resource &&
2054 $U->is_true($self->reservation->target_resource_type->catalog_item)
2056 # We used to try to set $self->copy and $self->patron here,
2057 # but that should already be done.
2059 $self->run_checkout_scripts(1);
2061 my $duration = $self->duration_rule;
2062 my $max = $self->max_fine_rule;
2063 my $recurring = $self->recurring_fines_rule;
2065 if ($duration && $max && $recurring) {
2066 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2068 my $dname = $duration->name;
2069 my $mname = $max->name;
2070 my $rname = $recurring->name;
2072 $logger->debug("circulator: updating reservation ".
2073 "with duration=$dname, maxfine=$mname, recurring=$rname");
2075 $self->reservation->fine_amount($policy->{recurring_fine});
2076 $self->reservation->max_fine($policy->{max_fine});
2077 $self->reservation->fine_interval($recurring->recurrence_interval);
2080 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2081 $self->update_copy();
2084 $self->reservation->fine_amount(
2085 $self->reservation->target_resource_type->fine_amount
2087 $self->reservation->max_fine(
2088 $self->reservation->target_resource_type->max_fine
2090 $self->reservation->fine_interval(
2091 $self->reservation->target_resource_type->fine_interval
2095 $self->update_reservation();
2098 sub do_reservation_return {
2100 my $request = shift;
2102 $self->log_me("do_reservation_return()");
2104 if (not ref $self->reservation) {
2105 my ($reservation, $evt) =
2106 $U->fetch_booking_reservation($self->reservation);
2107 return $self->bail_on_events($evt) if $evt;
2108 $self->reservation($reservation);
2111 $self->generate_fines(1);
2112 $self->reservation->return_time('now');
2113 $self->update_reservation();
2114 $self->reshelve_copy if $self->copy;
2116 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2117 $self->copy( $self->reservation->current_resource->catalog_item );
2121 sub booking_adjusted_due_date {
2123 my $circ = $self->circ;
2124 my $copy = $self->copy;
2126 return undef unless $self->use_booking;
2130 if( $self->due_date ) {
2132 return $self->bail_on_events($self->editor->event)
2133 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2135 $circ->due_date(cleanse_ISO8601($self->due_date));
2139 return unless $copy and $circ->due_date;
2142 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2143 if (@$booking_items) {
2144 my $booking_item = $booking_items->[0];
2145 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2147 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2148 my $shorten_circ_setting = $resource_type->elbow_room ||
2149 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2152 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2153 my $bookings = $booking_ses->request(
2154 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2155 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2157 $booking_ses->disconnect;
2159 my $dt_parser = DateTime::Format::ISO8601->new;
2160 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2162 for my $bid (@$bookings) {
2164 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2166 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2167 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2169 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2170 if ($booking_start < DateTime->now);
2173 if ($U->is_true($stop_circ_setting)) {
2174 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2176 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2177 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2180 # We set the circ duration here only to affect the logic that will
2181 # later (in a DB trigger) mangle the time part of the due date to
2182 # 11:59pm. Having any circ duration that is not a whole number of
2183 # days is enough to prevent the "correction."
2184 my $new_circ_duration = $due_date->epoch - time;
2185 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2186 $circ->duration("$new_circ_duration seconds");
2188 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2192 return $self->bail_on_events($self->editor->event)
2193 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2199 sub apply_modified_due_date {
2201 my $shift_earlier = shift;
2202 my $circ = $self->circ;
2203 my $copy = $self->copy;
2205 if( $self->due_date ) {
2207 return $self->bail_on_events($self->editor->event)
2208 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2210 $circ->due_date(cleanse_ISO8601($self->due_date));
2214 # if the due_date lands on a day when the location is closed
2215 return unless $copy and $circ->due_date;
2217 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2219 # due-date overlap should be determined by the location the item
2220 # is checked out from, not the owning or circ lib of the item
2221 my $org = $self->circ_lib;
2223 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2224 " with an item due date of ".$circ->due_date );
2226 my $dateinfo = $U->storagereq(
2227 'open-ils.storage.actor.org_unit.closed_date.overlap',
2228 $org, $circ->due_date );
2231 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2232 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2234 # XXX make the behavior more dynamic
2235 # for now, we just push the due date to after the close date
2236 if ($shift_earlier) {
2237 $circ->due_date($dateinfo->{start});
2239 $circ->due_date($dateinfo->{end});
2247 sub create_due_date {
2248 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2250 # if there is a raw time component (e.g. from postgres),
2251 # turn it into an interval that interval_to_seconds can parse
2252 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2254 # for now, use the server timezone. TODO: use workstation org timezone
2255 my $due_date = DateTime->now(time_zone => 'local');
2257 # add the circ duration
2258 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2261 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2262 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2263 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2268 # return ISO8601 time with timezone
2269 return $due_date->strftime('%FT%T%z');
2274 sub make_precat_copy {
2276 my $copy = $self->copy;
2279 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2281 $copy->editor($self->editor->requestor->id);
2282 $copy->edit_date('now');
2283 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2284 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2285 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2286 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2287 $self->update_copy();
2291 $logger->info("circulator: Creating a new precataloged ".
2292 "copy in checkout with barcode " . $self->copy_barcode);
2294 $copy = Fieldmapper::asset::copy->new;
2295 $copy->circ_lib($self->circ_lib);
2296 $copy->creator($self->editor->requestor->id);
2297 $copy->editor($self->editor->requestor->id);
2298 $copy->barcode($self->copy_barcode);
2299 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2300 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2301 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2303 $copy->dummy_title($self->dummy_title || "");
2304 $copy->dummy_author($self->dummy_author || "");
2305 $copy->dummy_isbn($self->dummy_isbn || "");
2306 $copy->circ_modifier($self->circ_modifier);
2309 # See if we need to override the circ_lib for the copy with a configured circ_lib
2310 # Setting is shortname of the org unit
2311 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2312 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2314 if($precat_circ_lib) {
2315 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2318 $self->bail_on_events($self->editor->event);
2322 $copy->circ_lib($org->id);
2326 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2328 $self->push_events($self->editor->event);
2332 # this is a little bit of a hack, but we need to
2333 # get the copy into the script runner
2334 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2338 sub checkout_noncat {
2344 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2345 my $count = $self->noncat_count || 1;
2346 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2348 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2352 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2353 $self->editor->requestor->id,
2361 $self->push_events($evt);
2369 # If a copy goes into transit and is then checked in before the transit checkin
2370 # interval has expired, push an event onto the overridable events list.
2371 sub check_transit_checkin_interval {
2374 # only concerned with in-transit items
2375 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2377 # no interval, no problem
2378 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2379 return unless $interval;
2381 # capture the transit so we don't have to fetch it again later during checkin
2383 $self->editor->search_action_transit_copy(
2384 {target_copy => $self->copy->id, dest_recv_time => undef}
2388 # transit from X to X for whatever reason has no min interval
2389 return if $self->transit->source == $self->transit->dest;
2391 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2392 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2393 my $horizon = $t_start->add(seconds => $seconds);
2395 # See if we are still within the transit checkin forbidden range
2396 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2397 if $horizon > DateTime->now;
2400 # Retarget local holds at checkin
2401 sub checkin_retarget {
2403 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2404 return unless $self->is_checkin; # Renewals need not be checked
2405 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2406 return if $self->is_precat; # No holds for precats
2407 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2408 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2409 my $status = $U->copy_status($self->copy->status);
2410 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2411 # Specifically target items that are likely new (by status ID)
2412 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2413 my $location = $self->copy->location;
2414 if(!ref($location)) {
2415 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2416 $self->copy->location($location);
2418 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2420 # Fetch holds for the bib
2421 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2422 $self->editor->authtoken,
2425 capture_time => undef, # No touching captured holds
2426 frozen => 'f', # Don't bother with frozen holds
2427 pickup_lib => $self->circ_lib # Only holds actually here
2430 # Error? Skip the step.
2431 return if exists $result->{"ilsevent"};
2435 foreach my $holdlist (keys %{$result}) {
2436 push @$holds, @{$result->{$holdlist}};
2439 return if scalar(@$holds) == 0; # No holds, no retargeting
2441 # Check for parts on this copy
2442 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2443 my %parts_hash = ();
2444 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2446 # Loop over holds in request-ish order
2447 # Stage 1: Get them into request-ish order
2448 # Also grab type and target for skipping low hanging ones
2449 $result = $self->editor->json_query({
2450 "select" => { "ahr" => ["id", "hold_type", "target"] },
2451 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2452 "where" => { "id" => $holds },
2454 { "class" => "pgt", "field" => "hold_priority"},
2455 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2456 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2457 { "class" => "ahr", "field" => "request_time"}
2462 if (ref $result eq "ARRAY" and scalar @$result) {
2463 foreach (@{$result}) {
2464 # Copy level, but not this copy?
2465 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2466 and $_->{target} != $self->copy->id);
2467 # Volume level, but not this volume?
2468 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2469 if(@$parts) { # We have parts?
2471 next if ($_->{hold_type} eq 'T');
2472 # Skip part holds for parts not on this copy
2473 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2475 # No parts, no part holds
2476 next if ($_->{hold_type} eq 'P');
2478 # So much for easy stuff, attempt a retarget!
2479 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2480 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2481 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2489 $self->log_me("do_checkin()");
2491 return $self->bail_on_events(
2492 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2495 $self->check_transit_checkin_interval;
2496 $self->checkin_retarget;
2498 # the renew code and mk_env should have already found our circulation object
2499 unless( $self->circ ) {
2501 my $circs = $self->editor->search_action_circulation(
2502 { target_copy => $self->copy->id, checkin_time => undef });
2504 $self->circ($$circs[0]);
2506 # for now, just warn if there are multiple open circs on a copy
2507 $logger->warn("circulator: we have ".scalar(@$circs).
2508 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2511 # run the fine generator against this circ, if this circ is there
2512 $self->generate_fines_start if $self->circ;
2514 if( $self->checkin_check_holds_shelf() ) {
2515 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2516 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2517 if($self->fake_hold_dest) {
2518 $self->hold->pickup_lib($self->circ_lib);
2520 $self->checkin_flesh_events;
2524 unless( $self->is_renewal ) {
2525 return $self->bail_on_events($self->editor->event)
2526 unless $self->editor->allowed('COPY_CHECKIN');
2529 $self->push_events($self->check_copy_alert());
2530 $self->push_events($self->check_checkin_copy_status());
2532 # if the circ is marked as 'claims returned', add the event to the list
2533 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2534 if ($self->circ and $self->circ->stop_fines
2535 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2537 $self->check_circ_deposit();
2539 # handle the overridable events
2540 $self->override_events unless $self->is_renewal;
2541 return if $self->bail_out;
2543 if( $self->copy and !$self->transit ) {
2545 $self->editor->search_action_transit_copy(
2546 { target_copy => $self->copy->id, dest_recv_time => undef }
2552 $self->generate_fines_finish;
2553 $self->checkin_handle_circ;
2554 return if $self->bail_out;
2555 $self->checkin_changed(1);
2557 } elsif( $self->transit ) {
2558 my $hold_transit = $self->process_received_transit;
2559 $self->checkin_changed(1);
2561 if( $self->bail_out ) {
2562 $self->checkin_flesh_events;
2566 if( my $e = $self->check_checkin_copy_status() ) {
2567 # If the original copy status is special, alert the caller
2568 my $ev = $self->events;
2569 $self->events([$e]);
2570 $self->override_events;
2571 return if $self->bail_out;
2575 if( $hold_transit or
2576 $U->copy_status($self->copy->status)->id
2577 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2580 if( $hold_transit ) {
2581 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2583 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2588 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2590 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2591 $self->reshelve_copy(1);
2592 $self->cancelled_hold_transit(1);
2593 $self->notify_hold(0); # don't notify for cancelled holds
2594 $self->fake_hold_dest(0);
2595 return if $self->bail_out;
2597 } elsif ($hold and $hold->hold_type eq 'R') {
2599 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2600 $self->notify_hold(0); # No need to notify
2601 $self->fake_hold_dest(0);
2602 $self->noop(1); # Don't try and capture for other holds/transits now
2603 $self->update_copy();
2604 $hold->fulfillment_time('now');
2605 $self->bail_on_events($self->editor->event)
2606 unless $self->editor->update_action_hold_request($hold);
2610 # hold transited to correct location
2611 if($self->fake_hold_dest) {
2612 $hold->pickup_lib($self->circ_lib);
2614 $self->checkin_flesh_events;
2619 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2621 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2622 " that is in-transit, but there is no transit.. repairing");
2623 $self->reshelve_copy(1);
2624 return if $self->bail_out;
2627 if( $self->is_renewal ) {
2628 $self->finish_fines_and_voiding;
2629 return if $self->bail_out;
2630 $self->push_events(OpenILS::Event->new('SUCCESS'));
2634 # ------------------------------------------------------------------------------
2635 # Circulations and transits are now closed where necessary. Now go on to see if
2636 # this copy can fulfill a hold or needs to be routed to a different location
2637 # ------------------------------------------------------------------------------
2639 my $needed_for_something = 0; # formerly "needed_for_hold"
2641 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2643 if (!$self->remote_hold) {
2644 if ($self->use_booking) {
2645 my $potential_hold = $self->hold_capture_is_possible;
2646 my $potential_reservation = $self->reservation_capture_is_possible;
2648 if ($potential_hold and $potential_reservation) {
2649 $logger->info("circulator: item could fulfill either hold or reservation");
2650 $self->push_events(new OpenILS::Event(
2651 "HOLD_RESERVATION_CONFLICT",
2652 "hold" => $potential_hold,
2653 "reservation" => $potential_reservation
2655 return if $self->bail_out;
2656 } elsif ($potential_hold) {
2657 $needed_for_something =
2658 $self->attempt_checkin_hold_capture;
2659 } elsif ($potential_reservation) {
2660 $needed_for_something =
2661 $self->attempt_checkin_reservation_capture;
2664 $needed_for_something = $self->attempt_checkin_hold_capture;
2667 return if $self->bail_out;
2669 unless($needed_for_something) {
2670 my $circ_lib = (ref $self->copy->circ_lib) ?
2671 $self->copy->circ_lib->id : $self->copy->circ_lib;
2673 if( $self->remote_hold ) {
2674 $circ_lib = $self->remote_hold->pickup_lib;
2675 $logger->warn("circulator: Copy ".$self->copy->barcode.
2676 " is on a remote hold's shelf, sending to $circ_lib");
2679 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2681 my $suppress_transit = 0;
2683 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2684 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2685 if($suppress_transit_source && $suppress_transit_source->{value}) {
2686 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2687 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2688 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2689 $suppress_transit = 1;
2694 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2695 # copy is where it needs to be, either for hold or reshelving
2697 $self->checkin_handle_precat();
2698 return if $self->bail_out;
2701 # copy needs to transit "home", or stick here if it's a floating copy
2703 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2704 $self->checkin_changed(1);
2705 $self->copy->circ_lib( $self->circ_lib );
2708 my $bc = $self->copy->barcode;
2709 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2710 $self->checkin_build_copy_transit($circ_lib);
2711 return if $self->bail_out;
2712 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2716 } else { # no-op checkin
2717 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2718 $self->checkin_changed(1);
2719 $self->copy->circ_lib( $self->circ_lib );
2724 if($self->claims_never_checked_out and
2725 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2727 # the item was not supposed to be checked out to the user and should now be marked as missing
2728 $self->copy->status(OILS_COPY_STATUS_MISSING);
2732 $self->reshelve_copy unless $needed_for_something;
2735 return if $self->bail_out;
2737 unless($self->checkin_changed) {
2739 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2740 my $stat = $U->copy_status($self->copy->status)->id;
2742 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2743 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2744 $self->bail_out(1); # no need to commit anything
2748 $self->push_events(OpenILS::Event->new('SUCCESS'))
2749 unless @{$self->events};
2752 $self->finish_fines_and_voiding;
2754 OpenILS::Utils::Penalty->calculate_penalties(
2755 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2757 $self->checkin_flesh_events;
2761 sub finish_fines_and_voiding {
2763 return unless $self->circ;
2765 # gather any updates to the circ after fine generation, if there was a circ
2766 $self->generate_fines_finish;
2768 return unless $self->backdate or $self->void_overdues;
2770 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2771 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2773 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2774 $self->editor, $self->circ, $self->backdate, $note);
2776 return $self->bail_on_events($evt) if $evt;
2778 # make sure the circ isn't closed if we just voided some fines
2779 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2780 return $self->bail_on_events($evt) if $evt;
2786 # if a deposit was payed for this item, push the event
2787 sub check_circ_deposit {
2789 return unless $self->circ;
2790 my $deposit = $self->editor->search_money_billing(
2792 xact => $self->circ->id,
2794 }, {idlist => 1})->[0];
2796 $self->push_events(OpenILS::Event->new(
2797 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2802 my $force = $self->force || shift;
2803 my $copy = $self->copy;
2805 my $stat = $U->copy_status($copy->status)->id;
2808 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2809 $stat != OILS_COPY_STATUS_CATALOGING and
2810 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2811 $stat != OILS_COPY_STATUS_RESHELVING )) {
2813 $copy->status( OILS_COPY_STATUS_RESHELVING );
2815 $self->checkin_changed(1);
2820 # Returns true if the item is at the current location
2821 # because it was transited there for a hold and the
2822 # hold has not been fulfilled
2823 sub checkin_check_holds_shelf {
2825 return 0 unless $self->copy;
2828 $U->copy_status($self->copy->status)->id ==
2829 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2831 # Attempt to clear shelf expired holds for this copy
2832 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2833 if($self->clear_expired);
2835 # find the hold that put us on the holds shelf
2836 my $holds = $self->editor->search_action_hold_request(
2838 current_copy => $self->copy->id,
2839 capture_time => { '!=' => undef },
2840 fulfillment_time => undef,
2841 cancel_time => undef,
2846 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2847 $self->reshelve_copy(1);
2851 my $hold = $$holds[0];
2853 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2854 $hold->id. "] for copy ".$self->copy->barcode);
2856 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2857 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2858 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2859 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2860 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2861 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2862 $self->fake_hold_dest(1);
2868 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2869 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2873 $logger->info("circulator: hold is not for here..");
2874 $self->remote_hold($hold);
2879 sub checkin_handle_precat {
2881 my $copy = $self->copy;
2883 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2884 $copy->status(OILS_COPY_STATUS_CATALOGING);
2885 $self->update_copy();
2886 $self->checkin_changed(1);
2887 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2892 sub checkin_build_copy_transit {
2895 my $copy = $self->copy;
2896 my $transit = Fieldmapper::action::transit_copy->new;
2898 # if we are transiting an item to the shelf shelf, it's a hold transit
2899 if (my $hold = $self->remote_hold) {
2900 $transit = Fieldmapper::action::hold_transit_copy->new;
2901 $transit->hold($hold->id);
2903 # the item is going into transit, remove any shelf-iness
2904 if ($hold->current_shelf_lib or $hold->shelf_time) {
2905 $hold->clear_current_shelf_lib;
2906 $hold->clear_shelf_time;
2907 return $self->bail_on_events($self->editor->event)
2908 unless $self->editor->update_action_hold_request($hold);
2912 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2913 $logger->info("circulator: transiting copy to $dest");
2915 $transit->source($self->circ_lib);
2916 $transit->dest($dest);
2917 $transit->target_copy($copy->id);
2918 $transit->source_send_time('now');
2919 $transit->copy_status( $U->copy_status($copy->status)->id );
2921 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2923 if ($self->remote_hold) {
2924 return $self->bail_on_events($self->editor->event)
2925 unless $self->editor->create_action_hold_transit_copy($transit);
2927 return $self->bail_on_events($self->editor->event)
2928 unless $self->editor->create_action_transit_copy($transit);
2931 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2933 $self->checkin_changed(1);
2937 sub hold_capture_is_possible {
2939 my $copy = $self->copy;
2941 # we've been explicitly told not to capture any holds
2942 return 0 if $self->capture eq 'nocapture';
2944 # See if this copy can fulfill any holds
2945 my $hold = $holdcode->find_nearest_permitted_hold(
2946 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2948 return undef if ref $hold eq "HASH" and
2949 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2953 sub reservation_capture_is_possible {
2955 my $copy = $self->copy;
2957 # we've been explicitly told not to capture any holds
2958 return 0 if $self->capture eq 'nocapture';
2960 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2961 my $resv = $booking_ses->request(
2962 "open-ils.booking.reservations.could_capture",
2963 $self->editor->authtoken, $copy->barcode
2965 $booking_ses->disconnect;
2966 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2967 $self->push_events($resv);
2973 # returns true if the item was used (or may potentially be used
2974 # in subsequent calls) to capture a hold.
2975 sub attempt_checkin_hold_capture {
2977 my $copy = $self->copy;
2979 # we've been explicitly told not to capture any holds
2980 return 0 if $self->capture eq 'nocapture';
2982 # See if this copy can fulfill any holds
2983 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2984 $self->editor, $copy, $self->editor->requestor );
2987 $logger->debug("circulator: no potential permitted".
2988 "holds found for copy ".$copy->barcode);
2992 if($self->capture ne 'capture') {
2993 # see if this item is in a hold-capture-delay location
2994 my $location = $self->copy->location;
2995 if(!ref($location)) {
2996 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2997 $self->copy->location($location);
2999 if($U->is_true($location->hold_verify)) {
3000 $self->bail_on_events(
3001 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3006 $self->retarget($retarget);
3008 my $suppress_transit = 0;
3009 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3010 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3011 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3012 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3013 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3014 $suppress_transit = 1;
3015 $self->hold->pickup_lib($self->circ_lib);
3020 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3022 $hold->current_copy($copy->id);
3023 $hold->capture_time('now');
3024 $self->put_hold_on_shelf($hold)
3025 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3027 # prevent DB errors caused by fetching
3028 # holds from storage, and updating through cstore
3029 $hold->clear_fulfillment_time;
3030 $hold->clear_fulfillment_staff;
3031 $hold->clear_fulfillment_lib;
3032 $hold->clear_expire_time;
3033 $hold->clear_cancel_time;
3034 $hold->clear_prev_check_time unless $hold->prev_check_time;
3036 $self->bail_on_events($self->editor->event)
3037 unless $self->editor->update_action_hold_request($hold);
3039 $self->checkin_changed(1);
3041 return 0 if $self->bail_out;
3043 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3045 if ($hold->hold_type eq 'R') {
3046 $copy->status(OILS_COPY_STATUS_CATALOGING);
3047 $hold->fulfillment_time('now');
3048 $self->noop(1); # Block other transit/hold checks
3049 $self->bail_on_events($self->editor->event)
3050 unless $self->editor->update_action_hold_request($hold);
3052 # This hold was captured in the correct location
3053 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3054 $self->push_events(OpenILS::Event->new('SUCCESS'));
3056 #$self->do_hold_notify($hold->id);
3057 $self->notify_hold($hold->id);
3062 # Hold needs to be picked up elsewhere. Build a hold
3063 # transit and route the item.
3064 $self->checkin_build_hold_transit();
3065 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3066 return 0 if $self->bail_out;
3067 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3070 # make sure we save the copy status
3072 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3076 sub attempt_checkin_reservation_capture {
3078 my $copy = $self->copy;
3080 # we've been explicitly told not to capture any holds
3081 return 0 if $self->capture eq 'nocapture';
3083 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3084 my $evt = $booking_ses->request(
3085 "open-ils.booking.resources.capture_for_reservation",
3086 $self->editor->authtoken,
3088 1 # don't update copy - we probably have it locked
3090 $booking_ses->disconnect;
3092 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3094 "open-ils.booking.resources.capture_for_reservation " .
3095 "didn't return an event!"
3099 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3100 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3102 # not-transferable is an error event we'll pass on the user
3103 $logger->warn("reservation capture attempted against non-transferable item");
3104 $self->push_events($evt);
3106 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3107 # Re-retrieve copy as reservation capture may have changed
3108 # its status and whatnot.
3110 "circulator: booking capture win on copy " . $self->copy->id
3112 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3114 "circulator: changing copy " . $self->copy->id .
3115 "'s status from " . $self->copy->status . " to " .
3118 $self->copy->status($new_copy_status);
3121 $self->reservation($evt->{"payload"}->{"reservation"});
3123 if (exists $evt->{"payload"}->{"transit"}) {
3127 "org" => $evt->{"payload"}->{"transit"}->dest
3131 $self->checkin_changed(1);
3135 # other results are treated as "nothing to capture"
3139 sub do_hold_notify {
3140 my( $self, $holdid ) = @_;
3142 my $e = new_editor(xact => 1);
3143 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3145 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3146 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3148 $logger->info("circulator: running delayed hold notify process");
3150 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3151 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3153 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3154 hold_id => $holdid, requestor => $self->editor->requestor);
3156 $logger->debug("circulator: built hold notifier");
3158 if(!$notifier->event) {
3160 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3162 my $stat = $notifier->send_email_notify;
3163 if( $stat == '1' ) {
3164 $logger->info("circulator: hold notify succeeded for hold $holdid");
3168 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3171 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3175 sub retarget_holds {
3177 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3178 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3179 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3180 # no reason to wait for the return value
3184 sub checkin_build_hold_transit {
3187 my $copy = $self->copy;
3188 my $hold = $self->hold;
3189 my $trans = Fieldmapper::action::hold_transit_copy->new;
3191 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3193 $trans->hold($hold->id);
3194 $trans->source($self->circ_lib);
3195 $trans->dest($hold->pickup_lib);
3196 $trans->source_send_time("now");
3197 $trans->target_copy($copy->id);
3199 # when the copy gets to its destination, it will recover
3200 # this status - put it onto the holds shelf
3201 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3203 return $self->bail_on_events($self->editor->event)
3204 unless $self->editor->create_action_hold_transit_copy($trans);
3209 sub process_received_transit {
3211 my $copy = $self->copy;
3212 my $copyid = $self->copy->id;
3214 my $status_name = $U->copy_status($copy->status)->name;
3215 $logger->debug("circulator: attempting transit receive on ".
3216 "copy $copyid. Copy status is $status_name");
3218 my $transit = $self->transit;
3220 # Check if we are in a transit suppress range
3221 my $suppress_transit = 0;
3222 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3223 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3224 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3225 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3226 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3227 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3228 $suppress_transit = 1;
3229 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3233 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3234 # - this item is in-transit to a different location
3235 # - Or we are capturing holds as transits, so why create a new transit?
3237 my $tid = $transit->id;
3238 my $loc = $self->circ_lib;
3239 my $dest = $transit->dest;
3241 $logger->info("circulator: Fowarding transit on copy which is destined ".
3242 "for a different location. transit=$tid, copy=$copyid, current ".
3243 "location=$loc, destination location=$dest");
3245 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3247 # grab the associated hold object if available
3248 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3249 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3251 return $self->bail_on_events($evt);
3254 # The transit is received, set the receive time
3255 $transit->dest_recv_time('now');
3256 $self->bail_on_events($self->editor->event)
3257 unless $self->editor->update_action_transit_copy($transit);
3259 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3261 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3262 $copy->status( $transit->copy_status );
3263 $self->update_copy();
3264 return if $self->bail_out;
3268 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3270 # hold has arrived at destination, set shelf time
3271 $self->put_hold_on_shelf($hold);
3272 $self->bail_on_events($self->editor->event)
3273 unless $self->editor->update_action_hold_request($hold);
3274 return if $self->bail_out;
3276 $self->notify_hold($hold_transit->hold);
3281 OpenILS::Event->new(
3284 payload => { transit => $transit, holdtransit => $hold_transit } ));
3286 return $hold_transit;
3290 # ------------------------------------------------------------------
3291 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3292 # ------------------------------------------------------------------
3293 sub put_hold_on_shelf {
3294 my($self, $hold) = @_;
3295 $hold->shelf_time('now');
3296 $hold->current_shelf_lib($self->circ_lib);
3297 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3303 sub generate_fines {
3305 my $reservation = shift;
3307 $self->generate_fines_start($reservation);
3308 $self->generate_fines_finish($reservation);
3313 sub generate_fines_start {
3315 my $reservation = shift;
3316 my $dt_parser = DateTime::Format::ISO8601->new;
3318 my $obj = $reservation ? $self->reservation : $self->circ;
3320 # If we have a grace period
3321 if($obj->can('grace_period')) {
3322 # Parse out the due date
3323 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3324 # Add the grace period to the due date
3325 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3326 # Don't generate fines on circs still in grace period
3327 return undef if ($due_date > DateTime->now);
3330 if (!exists($self->{_gen_fines_req})) {
3331 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3333 'open-ils.storage.action.circulation.overdue.generate_fines',
3341 sub generate_fines_finish {
3343 my $reservation = shift;
3345 return undef unless $self->{_gen_fines_req};
3347 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3349 $self->{_gen_fines_req}->wait_complete;
3350 delete($self->{_gen_fines_req});
3352 # refresh the circ in case the fine generator set the stop_fines field
3353 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3354 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3359 sub checkin_handle_circ {
3361 my $circ = $self->circ;
3362 my $copy = $self->copy;
3366 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3368 # backdate the circ if necessary
3369 if($self->backdate) {
3370 my $evt = $self->checkin_handle_backdate;
3371 return $self->bail_on_events($evt) if $evt;
3374 if(!$circ->stop_fines) {
3375 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3376 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3377 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3378 $circ->stop_fines_time('now');
3379 $circ->stop_fines_time($self->backdate) if $self->backdate;
3382 # Set the checkin vars since we have the item
3383 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3385 # capture the true scan time for back-dated checkins
3386 $circ->checkin_scan_time('now');
3388 $circ->checkin_staff($self->editor->requestor->id);
3389 $circ->checkin_lib($self->circ_lib);
3390 $circ->checkin_workstation($self->editor->requestor->wsid);
3392 my $circ_lib = (ref $self->copy->circ_lib) ?
3393 $self->copy->circ_lib->id : $self->copy->circ_lib;
3394 my $stat = $U->copy_status($self->copy->status)->id;
3396 if ($stat == OILS_COPY_STATUS_LOST) {
3397 # we will now handle lost fines, but the copy will retain its 'lost'
3398 # status if it needs to transit home unless lost_immediately_available
3401 # if we decide to also delay fine handling until the item arrives home,
3402 # we will need to call lost fine handling code both when checking items
3403 # in and also when receiving transits
3404 $self->checkin_handle_lost($circ_lib);
3405 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3406 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3408 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3413 # see if there are any fines owed on this circ. if not, close it
3414 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3415 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3417 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3419 return $self->bail_on_events($self->editor->event)
3420 unless $self->editor->update_action_circulation($circ);
3426 # ------------------------------------------------------------------
3427 # See if we need to void billings for lost checkin
3428 # ------------------------------------------------------------------
3429 sub checkin_handle_lost {
3431 my $circ_lib = shift;
3432 my $circ = $self->circ;
3434 my $max_return = $U->ou_ancestor_setting_value(
3435 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3440 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3441 $tm[5] -= 1 if $tm[5] > 0;
3442 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3444 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3445 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3447 $max_return = 0 if $today < $last_chance;
3450 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3452 my $void_lost = $U->ou_ancestor_setting_value(
3453 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3454 my $void_lost_fee = $U->ou_ancestor_setting_value(
3455 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3456 my $restore_od = $U->ou_ancestor_setting_value(
3457 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3458 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3459 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3461 $self->checkin_handle_lost_now_found(3) if $void_lost;
3462 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3463 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3466 if ($circ_lib != $self->circ_lib) {
3467 # if the item is not home, check to see if we want to retain the lost
3468 # status at this point in the process
3469 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3471 if ($immediately_available) {
3472 # lost item status does not need to be retained, so give it a
3473 # reshelving status as if it were a normal checkin
3474 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3477 $logger->info("circulator: not updating copy status on checkin because copy is lost");
3480 # lost item is home and processed, treat like a normal checkin from
3482 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3488 sub checkin_handle_backdate {
3491 # ------------------------------------------------------------------
3492 # clean up the backdate for date comparison
3493 # XXX We are currently taking the due-time from the original due-date,
3494 # not the input. Do we need to do this? This certainly interferes with
3495 # backdating of hourly checkouts, but that is likely a very rare case.
3496 # ------------------------------------------------------------------
3497 my $bd = cleanse_ISO8601($self->backdate);
3498 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3499 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3500 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3502 $self->backdate($bd);
3507 sub check_checkin_copy_status {
3509 my $copy = $self->copy;
3511 my $status = $U->copy_status($copy->status)->id;
3514 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3515 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3516 $status == OILS_COPY_STATUS_IN_PROCESS ||
3517 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3518 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3519 $status == OILS_COPY_STATUS_CATALOGING ||
3520 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3521 $status == OILS_COPY_STATUS_RESHELVING );
3523 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3524 if( $status == OILS_COPY_STATUS_LOST );
3526 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3527 if( $status == OILS_COPY_STATUS_MISSING );
3529 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3534 # --------------------------------------------------------------------------
3535 # On checkin, we need to return as many relevant objects as we can
3536 # --------------------------------------------------------------------------
3537 sub checkin_flesh_events {
3540 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3541 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3542 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3545 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3548 if($self->hold and !$self->hold->cancel_time) {
3549 $hold = $self->hold;
3550 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3554 # if we checked in a circulation, flesh the billing summary data
3555 $self->circ->billable_transaction(
3556 $self->editor->retrieve_money_billable_transaction([
3558 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3564 # flesh some patron fields before returning
3566 $self->editor->retrieve_actor_user([
3571 au => ['card', 'billing_address', 'mailing_address']
3578 for my $evt (@{$self->events}) {
3581 $payload->{copy} = $U->unflesh_copy($self->copy);
3582 $payload->{volume} = $self->volume;
3583 $payload->{record} = $record,
3584 $payload->{circ} = $self->circ;
3585 $payload->{transit} = $self->transit;
3586 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3587 $payload->{hold} = $hold;
3588 $payload->{patron} = $self->patron;
3589 $payload->{reservation} = $self->reservation
3590 unless (not $self->reservation or $self->reservation->cancel_time);
3592 $evt->{payload} = $payload;
3597 my( $self, $msg ) = @_;
3598 my $bc = ($self->copy) ? $self->copy->barcode :
3601 my $usr = ($self->patron) ? $self->patron->id : "";
3602 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3603 ", recipient=$usr, copy=$bc");
3609 $self->log_me("do_renew()");
3611 # Make sure there is an open circ to renew that is not
3612 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3613 my $usrid = $self->patron->id if $self->patron;
3614 my $circ = $self->editor->search_action_circulation({
3615 target_copy => $self->copy->id,
3616 xact_finish => undef,
3617 checkin_time => undef,
3618 ($usrid ? (usr => $usrid) : ()),
3620 {stop_fines => undef},
3621 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3625 return $self->bail_on_events($self->editor->event) unless $circ;
3627 # A user is not allowed to renew another user's items without permission
3628 unless( $circ->usr eq $self->editor->requestor->id ) {
3629 return $self->bail_on_events($self->editor->events)
3630 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3633 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3634 if $circ->renewal_remaining < 1;
3636 # -----------------------------------------------------------------
3638 $self->parent_circ($circ->id);
3639 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3642 # Opac renewal - re-use circ library from original circ (unless told not to)
3643 if($self->opac_renewal) {
3644 unless(defined($opac_renewal_use_circ_lib)) {
3645 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3646 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3647 $opac_renewal_use_circ_lib = 1;
3650 $opac_renewal_use_circ_lib = 0;
3653 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3656 # Run the fine generator against the old circ
3657 $self->generate_fines_start;
3659 $self->run_renew_permit;
3662 $self->do_checkin();
3663 return if $self->bail_out;
3665 unless( $self->permit_override ) {
3667 return if $self->bail_out;
3668 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3669 $self->remove_event('ITEM_NOT_CATALOGED');
3672 $self->override_events;
3673 return if $self->bail_out;
3676 $self->do_checkout();
3681 my( $self, $evt ) = @_;
3682 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3683 $logger->debug("circulator: removing event from list: $evt");
3684 my @events = @{$self->events};
3685 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3690 my( $self, $evt ) = @_;
3691 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3692 return grep { $_->{textcode} eq $evt } @{$self->events};
3697 sub run_renew_permit {
3700 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3701 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3702 $self->editor, $self->copy, $self->editor->requestor, 1
3704 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3707 if(!$self->legacy_script_support) {
3708 my $results = $self->run_indb_circ_test;
3709 $self->push_events($self->matrix_test_result_events)
3710 unless $self->circ_test_success;
3713 my $runner = $self->script_runner;
3715 $runner->load($self->circ_permit_renew);
3716 my $result = $runner->run or
3717 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3718 if ($result->{"events"}) {
3720 map { new OpenILS::Event($_) } @{$result->{"events"}}
3723 "circulator: circ_permit_renew for user " .
3724 $self->patron->id . " returned " .
3725 scalar(@{$result->{"events"}}) . " event(s)"
3729 $self->mk_script_runner;
3732 $logger->debug("circulator: re-creating script runner to be safe");
3736 # XXX: The primary mechanism for storing circ history is now handled
3737 # by tracking real circulation objects instead of bibs in a bucket.
3738 # However, this code is disabled by default and could be useful
3739 # some day, so may as well leave it for now.
3740 sub append_reading_list {
3744 $self->is_checkout and
3750 # verify history is globally enabled and uses the bucket mechanism
3751 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3752 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3754 return undef unless $htype and $htype eq 'bucket';
3756 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3758 # verify the patron wants to retain the hisory
3759 my $setting = $e->search_actor_user_setting(
3760 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3762 unless($setting and $setting->value) {
3767 my $bkt = $e->search_container_copy_bucket(
3768 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3773 # find the next item position
3774 my $last_item = $e->search_container_copy_bucket_item(
3775 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3776 $pos = $last_item->pos + 1 if $last_item;
3779 # create the history bucket if necessary
3780 $bkt = Fieldmapper::container::copy_bucket->new;
3781 $bkt->owner($self->patron->id);
3783 $bkt->btype('circ_history');
3785 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3788 my $item = Fieldmapper::container::copy_bucket_item->new;
3790 $item->bucket($bkt->id);
3791 $item->target_copy($self->copy->id);
3794 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3801 sub make_trigger_events {
3803 return unless $self->circ;
3804 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3805 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3806 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3811 sub checkin_handle_lost_now_found {
3812 my ($self, $bill_type) = @_;
3814 # ------------------------------------------------------------------
3815 # remove charge from patron's account if lost item is returned
3816 # ------------------------------------------------------------------
3818 my $bills = $self->editor->search_money_billing(
3820 xact => $self->circ->id,
3825 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3826 for my $bill (@$bills) {
3827 if( !$U->is_true($bill->voided) ) {
3828 $logger->info("lost item returned - voiding bill ".$bill->id);
3830 $bill->void_time('now');
3831 $bill->voider($self->editor->requestor->id);
3832 my $note = ($bill->note) ? $bill->note . "\n" : '';
3833 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3835 $self->bail_on_events($self->editor->event)
3836 unless $self->editor->update_money_billing($bill);
3841 sub checkin_handle_lost_now_found_restore_od {
3843 my $circ_lib = shift;
3845 # ------------------------------------------------------------------
3846 # restore those overdue charges voided when item was set to lost
3847 # ------------------------------------------------------------------
3849 my $ods = $self->editor->search_money_billing(
3851 xact => $self->circ->id,
3856 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3857 for my $bill (@$ods) {
3858 if( $U->is_true($bill->voided) ) {
3859 $logger->info("lost item returned - restoring overdue ".$bill->id);
3861 $bill->clear_void_time;
3862 $bill->voider($self->editor->requestor->id);
3863 my $note = ($bill->note) ? $bill->note . "\n" : '';
3864 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3866 $self->bail_on_events($self->editor->event)
3867 unless $self->editor->update_money_billing($bill);
3872 # ------------------------------------------------------------------
3873 # Lost-then-found item checked in. This sub generates new overdue
3874 # fines, beyond the point of any existing and possibly voided
3875 # overdue fines, up to the point of final checkin time (or max fine
3877 # ------------------------------------------------------------------
3878 sub generate_lost_overdue_fines {
3880 my $circ = $self->circ;
3881 my $e = $self->editor;
3883 # Re-open the transaction so the fine generator can see it
3884 if($circ->xact_finish or $circ->stop_fines) {
3886 $circ->clear_xact_finish;
3887 $circ->clear_stop_fines;
3888 $circ->clear_stop_fines_time;
3889 $e->update_action_circulation($circ) or return $e->die_event;
3893 $e->xact_begin; # generate_fines expects an in-xact editor
3894 $self->generate_fines;
3895 $circ = $self->circ; # generate fines re-fetches the circ
3899 # Re-close the transaction if no money is owed
3900 my ($obt) = $U->fetch_mbts($circ->id, $e);
3901 if ($obt and $obt->balance_owed == 0) {
3902 $circ->xact_finish('now');
3906 # Set stop fines if the fine generator didn't have to
3907 unless($circ->stop_fines) {
3908 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3909 $circ->stop_fines_time('now');
3913 # update the event data sent to the caller within the transaction
3914 $self->checkin_flesh_events;
3917 $e->update_action_circulation($circ) or return $e->die_event;