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 $opac_renewal_use_circ_lib;
16 my $desk_renewal_use_circ_lib;
18 sub determine_booking_status {
19 unless (defined $booking_status) {
20 my $ses = create OpenSRF::AppSession("router");
21 $booking_status = grep {$_ eq "open-ils.booking"} @{
22 $ses->request("opensrf.router.info.class.list")->gather(1)
25 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
28 return $booking_status;
34 flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
39 __PACKAGE__->register_method(
40 method => "run_method",
41 api_name => "open-ils.circ.checkout.permit",
43 Determines if the given checkout can occur
44 @param authtoken The login session key
45 @param params A trailing hash of named params including
46 barcode : The copy barcode,
47 patron : The patron the checkout is occurring for,
48 renew : true or false - whether or not this is a renewal
49 @return The event that occurred during the permit check.
53 __PACKAGE__->register_method (
54 method => 'run_method',
55 api_name => 'open-ils.circ.checkout.permit.override',
56 signature => q/@see open-ils.circ.checkout.permit/,
60 __PACKAGE__->register_method(
61 method => "run_method",
62 api_name => "open-ils.circ.checkout",
65 @param authtoken The login session key
66 @param params A named hash of params including:
68 barcode If no copy is provided, the copy is retrieved via barcode
69 copyid If no copy or barcode is provide, the copy id will be use
70 patron The patron's id
71 noncat True if this is a circulation for a non-cataloted item
72 noncat_type The non-cataloged type id
73 noncat_circ_lib The location for the noncat circ.
74 precat The item has yet to be cataloged
75 dummy_title The temporary title of the pre-cataloded item
76 dummy_author The temporary authr of the pre-cataloded item
77 Default is the home org of the staff member
78 @return The SUCCESS event on success, any other event depending on the error
81 __PACKAGE__->register_method(
82 method => "run_method",
83 api_name => "open-ils.circ.checkin",
86 Generic super-method for handling all copies
87 @param authtoken The login session key
88 @param params Hash of named parameters including:
89 barcode - The copy barcode
90 force - If true, copies in bad statuses will be checked in and give good statuses
91 noop - don't capture holds or put items into transit
92 void_overdues - void all overdues for the circulation (aka amnesty)
97 __PACKAGE__->register_method(
98 method => "run_method",
99 api_name => "open-ils.circ.checkin.override",
100 signature => q/@see open-ils.circ.checkin/
103 __PACKAGE__->register_method(
104 method => "run_method",
105 api_name => "open-ils.circ.renew.override",
106 signature => q/@see open-ils.circ.renew/,
110 __PACKAGE__->register_method(
111 method => "run_method",
112 api_name => "open-ils.circ.renew",
113 notes => <<" NOTES");
114 PARAMS( authtoken, circ => circ_id );
115 open-ils.circ.renew(login_session, circ_object);
116 Renews the provided circulation. login_session is the requestor of the
117 renewal and if the logged in user is not the same as circ->usr, then
118 the logged in user must have RENEW_CIRC permissions.
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.checkout.full"
125 __PACKAGE__->register_method(
126 method => "run_method",
127 api_name => "open-ils.circ.checkout.full.override"
129 __PACKAGE__->register_method(
130 method => "run_method",
131 api_name => "open-ils.circ.reservation.pickup"
133 __PACKAGE__->register_method(
134 method => "run_method",
135 api_name => "open-ils.circ.reservation.return"
137 __PACKAGE__->register_method(
138 method => "run_method",
139 api_name => "open-ils.circ.reservation.return.override"
141 __PACKAGE__->register_method(
142 method => "run_method",
143 api_name => "open-ils.circ.checkout.inspect",
144 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
149 my( $self, $conn, $auth, $args ) = @_;
150 translate_legacy_args($args);
151 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
152 my $api = $self->api_name;
155 OpenILS::Application::Circ::Circulator->new($auth, %$args);
157 return circ_events($circulator) if $circulator->bail_out;
159 $circulator->use_booking(determine_booking_status());
161 # --------------------------------------------------------------------------
162 # First, check for a booking transit, as the barcode may not be a copy
163 # barcode, but a resource barcode, and nothing else in here will work
164 # --------------------------------------------------------------------------
166 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
167 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
168 if (@$resources) { # yes!
170 my $res_id_list = [ map { $_->id } @$resources ];
171 my $transit = $circulator->editor->search_action_reservation_transit_copy(
173 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
174 { order_by => { artc => 'source_send_time' }, limit => 1 }
176 )->[0]; # Any transit for this barcode?
178 if ($transit) { # yes! unwrap it.
180 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
181 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
183 my $success_event = new OpenILS::Event(
184 "SUCCESS", "payload" => {"reservation" => $reservation}
186 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
187 if (my $copy = $circulator->editor->search_asset_copy([
188 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
189 ])->[0]) { # got a copy
190 $copy->status( $transit->copy_status );
191 $copy->editor($circulator->editor->requestor->id);
192 $copy->edit_date('now');
193 $circulator->editor->update_asset_copy($copy);
194 $success_event->{"payload"}->{"record"} =
195 $U->record_to_mvr($copy->call_number->record);
196 $success_event->{"payload"}->{"volume"} = $copy->call_number;
197 $copy->call_number($copy->call_number->id);
198 $success_event->{"payload"}->{"copy"} = $copy;
202 $transit->dest_recv_time('now');
203 $circulator->editor->update_action_reservation_transit_copy( $transit );
205 $circulator->editor->commit;
206 # Formerly this branch just stopped here. Argh!
207 $conn->respond_complete($success_event);
215 # --------------------------------------------------------------------------
216 # Go ahead and load the script runner to make sure we have all
217 # of the objects we need
218 # --------------------------------------------------------------------------
220 if ($circulator->use_booking) {
221 $circulator->is_res_checkin($circulator->is_checkin(1))
222 if $api =~ /reservation.return/ or (
223 $api =~ /checkin/ and $circulator->seems_like_reservation()
226 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
229 $circulator->is_renewal(1) if $api =~ /renew/;
230 $circulator->is_checkin(1) if $api =~ /checkin/;
232 $circulator->mk_env();
233 $circulator->noop(1) if $circulator->claims_never_checked_out;
235 return circ_events($circulator) if $circulator->bail_out;
237 $circulator->override(1) if $api =~ /override/o;
239 if( $api =~ /checkout\.permit/ ) {
240 $circulator->do_permit();
242 } elsif( $api =~ /checkout.full/ ) {
244 # requesting a precat checkout implies that any required
245 # overrides have been performed. Go ahead and re-override.
246 $circulator->skip_permit_key(1);
247 $circulator->override(1) if $circulator->request_precat;
248 $circulator->do_permit();
249 $circulator->is_checkout(1);
250 unless( $circulator->bail_out ) {
251 $circulator->events([]);
252 $circulator->do_checkout();
255 } elsif( $circulator->is_res_checkout ) {
256 $circulator->do_reservation_pickup();
258 } elsif( $api =~ /inspect/ ) {
259 my $data = $circulator->do_inspect();
260 $circulator->editor->rollback;
263 } elsif( $api =~ /checkout/ ) {
264 $circulator->is_checkout(1);
265 $circulator->do_checkout();
267 } elsif( $circulator->is_res_checkin ) {
268 $circulator->do_reservation_return();
269 $circulator->do_checkin() if ($circulator->copy());
270 } elsif( $api =~ /checkin/ ) {
271 $circulator->do_checkin();
273 } elsif( $api =~ /renew/ ) {
274 $circulator->is_renewal(1);
275 $circulator->do_renew();
278 if( $circulator->bail_out ) {
281 # make sure no success event accidentally slip in
283 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
286 my @e = @{$circulator->events};
287 push( @ee, $_->{textcode} ) for @e;
288 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
290 $circulator->editor->rollback;
294 $circulator->editor->commit;
297 $conn->respond_complete(circ_events($circulator));
299 $circulator->script_runner->cleanup if $circulator->script_runner;
301 return undef if $circulator->bail_out;
303 $circulator->do_hold_notify($circulator->notify_hold)
304 if $circulator->notify_hold;
305 $circulator->retarget_holds if $circulator->retarget;
306 $circulator->append_reading_list;
307 $circulator->make_trigger_events;
314 my @e = @{$circ->events};
315 # if we have multiple events, SUCCESS should not be one of them;
316 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
317 return (@e == 1) ? $e[0] : \@e;
321 sub translate_legacy_args {
324 if( $$args{barcode} ) {
325 $$args{copy_barcode} = $$args{barcode};
326 delete $$args{barcode};
329 if( $$args{copyid} ) {
330 $$args{copy_id} = $$args{copyid};
331 delete $$args{copyid};
334 if( $$args{patronid} ) {
335 $$args{patron_id} = $$args{patronid};
336 delete $$args{patronid};
339 if( $$args{patron} and !ref($$args{patron}) ) {
340 $$args{patron_id} = $$args{patron};
341 delete $$args{patron};
345 if( $$args{noncat} ) {
346 $$args{is_noncat} = $$args{noncat};
347 delete $$args{noncat};
350 if( $$args{precat} ) {
351 $$args{is_precat} = $$args{request_precat} = $$args{precat};
352 delete $$args{precat};
358 # --------------------------------------------------------------------------
359 # This package actually manages all of the circulation logic
360 # --------------------------------------------------------------------------
361 package OpenILS::Application::Circ::Circulator;
362 use strict; use warnings;
363 use vars q/$AUTOLOAD/;
365 use OpenILS::Utils::Fieldmapper;
366 use OpenSRF::Utils::Cache;
367 use Digest::MD5 qw(md5_hex);
368 use DateTime::Format::ISO8601;
369 use OpenILS::Utils::PermitHold;
370 use OpenSRF::Utils qw/:datetime/;
371 use OpenSRF::Utils::SettingsClient;
372 use OpenILS::Application::Circ::Holds;
373 use OpenILS::Application::Circ::Transit;
374 use OpenSRF::Utils::Logger qw(:logger);
375 use OpenILS::Utils::CStoreEditor qw/:funcs/;
376 use OpenILS::Const qw/:const/;
377 use OpenILS::Utils::Penalty;
378 use OpenILS::Application::Circ::CircCommon;
381 my $CC = "OpenILS::Application::Circ::CircCommon";
382 my $holdcode = "OpenILS::Application::Circ::Holds";
383 my $transcode = "OpenILS::Application::Circ::Transit";
389 # --------------------------------------------------------------------------
390 # Add a pile of automagic getter/setter methods
391 # --------------------------------------------------------------------------
392 my @AUTOLOAD_FIELDS = qw/
439 recurring_fines_level
452 cancelled_hold_transit
459 circ_matrix_matchpoint
470 claims_never_checked_out
483 dont_change_lost_zero
485 needs_lost_bill_handling
491 my $type = ref($self) or die "$self is not an object";
493 my $name = $AUTOLOAD;
496 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
497 $logger->error("circulator: $type: invalid autoload field: $name");
498 die "$type: invalid autoload field: $name\n"
503 *{"${type}::${name}"} = sub {
506 $s->{$name} = $v if defined $v;
510 return $self->$name($data);
515 my( $class, $auth, %args ) = @_;
516 $class = ref($class) || $class;
517 my $self = bless( {}, $class );
520 $self->editor(new_editor(xact => 1, authtoken => $auth));
522 unless( $self->editor->checkauth ) {
523 $self->bail_on_events($self->editor->event);
527 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
529 $self->$_($args{$_}) for keys %args;
532 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
534 # if this is a renewal, default to desk_renewal
535 $self->desk_renewal(1) unless
536 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
538 $self->capture('') unless $self->capture;
540 unless(%user_groups) {
541 my $gps = $self->editor->retrieve_all_permission_grp_tree;
542 %user_groups = map { $_->id => $_ } @$gps;
549 # --------------------------------------------------------------------------
550 # True if we should discontinue processing
551 # --------------------------------------------------------------------------
553 my( $self, $bool ) = @_;
554 if( defined $bool ) {
555 $logger->info("circulator: BAILING OUT") if $bool;
556 $self->{bail_out} = $bool;
558 return $self->{bail_out};
563 my( $self, @evts ) = @_;
566 $e->{payload} = $self->copy if
567 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
569 $logger->info("circulator: pushing event ".$e->{textcode});
570 push( @{$self->events}, $e ) unless
571 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
577 return '' if $self->skip_permit_key;
578 my $key = md5_hex( time() . rand() . "$$" );
579 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
580 return $self->permit_key($key);
583 sub check_permit_key {
585 return 1 if $self->skip_permit_key;
586 my $key = $self->permit_key;
587 return 0 unless $key;
588 my $k = "oils_permit_key_$key";
589 my $one = $self->cache_handle->get_cache($k);
590 $self->cache_handle->delete_cache($k);
591 return ($one) ? 1 : 0;
594 sub seems_like_reservation {
597 # Some words about the following method:
598 # 1) It requires the VIEW_USER permission, but that's not an
599 # issue, right, since all staff should have that?
600 # 2) It returns only one reservation at a time, even if an item can be
601 # and is currently overbooked. Hmmm....
602 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
603 my $result = $booking_ses->request(
604 "open-ils.booking.reservations.by_returnable_resource_barcode",
605 $self->editor->authtoken,
608 $booking_ses->disconnect;
610 return $self->bail_on_events($result) if defined $U->event_code($result);
613 $self->reservation(shift @$result);
621 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
622 sub save_trimmed_copy {
623 my ($self, $copy) = @_;
626 $self->volume($copy->call_number);
627 $self->title($self->volume->record);
628 $self->copy->call_number($self->volume->id);
629 $self->volume->record($self->title->id);
630 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
631 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
632 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
633 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
639 my $e = $self->editor;
641 # --------------------------------------------------------------------------
642 # Grab the fleshed copy
643 # --------------------------------------------------------------------------
644 unless($self->is_noncat) {
647 $copy = $e->retrieve_asset_copy(
648 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
650 } elsif( $self->copy_barcode ) {
652 $copy = $e->search_asset_copy(
653 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
654 } elsif( $self->reservation ) {
655 my $res = $e->json_query(
657 "select" => {"acp" => ["id"]},
662 "field" => "barcode",
666 "field" => "current_resource"
674 "id" => (ref $self->reservation) ?
675 $self->reservation->id : $self->reservation
680 if (ref $res eq "ARRAY" and scalar @$res) {
681 $logger->info("circulator: mapped reservation " .
682 $self->reservation . " to copy " . $res->[0]->{"id"});
683 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
688 $self->save_trimmed_copy($copy);
690 # We can't renew if there is no copy
691 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
692 if $self->is_renewal;
697 # --------------------------------------------------------------------------
699 # --------------------------------------------------------------------------
703 flesh_fields => {au => [ qw/ card / ]}
706 if( $self->patron_id ) {
707 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
708 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
710 } elsif( $self->patron_barcode ) {
712 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
713 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
714 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
716 $patron = $e->retrieve_actor_user($card->usr)
717 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
719 # Use the card we looked up, not the patron's primary, for card active checks
720 $patron->card($card);
723 if( my $copy = $self->copy ) {
726 $flesh->{flesh_fields}->{circ} = ['usr'];
728 my $circ = $e->search_action_circulation([
729 {target_copy => $copy->id, checkin_time => undef}, $flesh
733 $patron = $circ->usr;
734 $circ->usr($patron->id); # de-flesh for consistency
740 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
741 unless $self->patron($patron) or $self->is_checkin;
743 unless($self->is_checkin) {
745 # Check for inactivity and patron reg. expiration
747 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
748 unless $U->is_true($patron->active);
750 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
751 unless $U->is_true($patron->card->active);
753 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
754 cleanse_ISO8601($patron->expire_date));
756 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
757 if( CORE::time > $expire->epoch ) ;
762 # --------------------------------------------------------------------------
763 # Does the circ permit work
764 # --------------------------------------------------------------------------
768 $self->log_me("do_permit()");
770 unless( $self->editor->requestor->id == $self->patron->id ) {
771 return $self->bail_on_events($self->editor->event)
772 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
775 $self->check_captured_holds();
776 $self->do_copy_checks();
777 return if $self->bail_out;
778 $self->run_patron_permit_scripts();
779 $self->run_copy_permit_scripts()
780 unless $self->is_precat or $self->is_noncat;
781 $self->check_item_deposit_events();
782 $self->override_events();
783 return if $self->bail_out;
785 if($self->is_precat and not $self->request_precat) {
788 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
789 return $self->bail_out(1) unless $self->is_renewal;
793 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
796 sub check_item_deposit_events {
798 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
799 if $self->is_deposit and not $self->is_deposit_exempt;
800 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
801 if $self->is_rental and not $self->is_rental_exempt;
804 # returns true if the user is not required to pay deposits
805 sub is_deposit_exempt {
807 my $pid = (ref $self->patron->profile) ?
808 $self->patron->profile->id : $self->patron->profile;
809 my $groups = $U->ou_ancestor_setting_value(
810 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
811 for my $grp (@$groups) {
812 return 1 if $self->is_group_descendant($grp, $pid);
817 # returns true if the user is not required to pay rental fees
818 sub is_rental_exempt {
820 my $pid = (ref $self->patron->profile) ?
821 $self->patron->profile->id : $self->patron->profile;
822 my $groups = $U->ou_ancestor_setting_value(
823 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
824 for my $grp (@$groups) {
825 return 1 if $self->is_group_descendant($grp, $pid);
830 sub is_group_descendant {
831 my($self, $p_id, $c_id) = @_;
832 return 0 unless defined $p_id and defined $c_id;
833 return 1 if $c_id == $p_id;
834 while(my $grp = $user_groups{$c_id}) {
835 $c_id = $grp->parent;
836 return 0 unless defined $c_id;
837 return 1 if $c_id == $p_id;
842 sub check_captured_holds {
844 my $copy = $self->copy;
845 my $patron = $self->patron;
847 return undef unless $copy;
849 my $s = $U->copy_status($copy->status)->id;
850 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
851 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
853 # Item is on the holds shelf, make sure it's going to the right person
854 my $hold = $self->editor->search_action_hold_request(
857 current_copy => $copy->id ,
858 capture_time => { '!=' => undef },
859 cancel_time => undef,
860 fulfillment_time => undef
866 if ($hold and $hold->usr == $patron->id) {
867 $self->checkout_is_for_hold(1);
871 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
873 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
879 my $copy = $self->copy;
882 my $stat = $U->copy_status($copy->status)->id;
884 # We cannot check out a copy if it is in-transit
885 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
886 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
889 $self->handle_claims_returned();
890 return if $self->bail_out;
892 # no claims returned circ was found, check if there is any open circ
893 unless( $self->is_renewal ) {
895 my $circs = $self->editor->search_action_circulation(
896 { target_copy => $copy->id, checkin_time => undef }
899 if(my $old_circ = $circs->[0]) { # an open circ was found
901 my $payload = {copy => $copy};
903 if($old_circ->usr == $self->patron->id) {
905 $payload->{old_circ} = $old_circ;
907 # If there is an open circulation on the checkout item and an auto-renew
908 # interval is defined, inform the caller that they should go
909 # ahead and renew the item instead of warning about open circulations.
911 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
913 'circ.checkout_auto_renew_age',
917 if($auto_renew_intvl) {
918 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
919 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
921 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
922 $payload->{auto_renew} = 1;
927 return $self->bail_on_events(
928 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
934 my $LEGACY_CIRC_EVENT_MAP = {
935 'no_item' => 'ITEM_NOT_CATALOGED',
936 'actor.usr.barred' => 'PATRON_BARRED',
937 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
938 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
939 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
940 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
941 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
942 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
943 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
944 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
945 'config.circ_matrix_test.total_copy_hold_ratio' =>
946 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
947 'config.circ_matrix_test.available_copy_hold_ratio' =>
948 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
952 # ---------------------------------------------------------------------
953 # This pushes any patron-related events into the list but does not
954 # set bail_out for any events
955 # ---------------------------------------------------------------------
956 sub run_patron_permit_scripts {
958 my $runner = $self->script_runner;
959 my $patronid = $self->patron->id;
964 my $results = $self->run_indb_circ_test;
965 unless($self->circ_test_success) {
968 if ($self->is_noncat) {
969 # no_item result is OK during noncat checkout
970 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
974 if ($self->checkout_is_for_hold) {
975 # if this checkout will fulfill a hold, ignore CIRC blocks
976 # and rely instead on the (later-checked) FULFILL block
978 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
979 my $fblock_pens = $self->editor->search_config_standing_penalty(
980 {name => [@pen_names], block_list => {like => '%CIRC%'}});
982 for my $res (@$results) {
983 my $name = $res->{fail_part} || '';
984 next if grep {$_->name eq $name} @$fblock_pens;
985 push(@trimmed_results, $res);
989 # not for hold or noncat
990 @trimmed_results = @$results;
994 # update the final set of test results
995 $self->matrix_test_result(\@trimmed_results);
997 push @allevents, $self->matrix_test_result_events;
1001 $_->{payload} = $self->copy if
1002 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1005 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1007 $self->push_events(@allevents);
1010 sub matrix_test_result_codes {
1012 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1015 sub matrix_test_result_events {
1018 my $event = new OpenILS::Event(
1019 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1021 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1023 } (@{$self->matrix_test_result});
1026 sub run_indb_circ_test {
1028 return $self->matrix_test_result if $self->matrix_test_result;
1030 my $dbfunc = ($self->is_renewal) ?
1031 'action.item_user_renew_test' : 'action.item_user_circ_test';
1033 if( $self->is_precat && $self->request_precat) {
1034 $self->make_precat_copy;
1035 return if $self->bail_out;
1038 my $results = $self->editor->json_query(
1042 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1048 $self->circ_test_success($U->is_true($results->[0]->{success}));
1050 if(my $mp = $results->[0]->{matchpoint}) {
1051 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1052 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1053 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1054 if(defined($results->[0]->{renewals})) {
1055 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1057 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1058 if(defined($results->[0]->{grace_period})) {
1059 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1061 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1062 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1063 # Grab the *last* response for limit_groups, where it is more likely to be filled
1064 $self->limit_groups($results->[-1]->{limit_groups});
1067 return $self->matrix_test_result($results);
1070 # ---------------------------------------------------------------------
1071 # given a use and copy, this will calculate the circulation policy
1072 # parameters. Only works with in-db circ.
1073 # ---------------------------------------------------------------------
1077 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1079 $self->run_indb_circ_test;
1082 circ_test_success => $self->circ_test_success,
1083 failure_events => [],
1084 failure_codes => [],
1085 matchpoint => $self->circ_matrix_matchpoint
1088 unless($self->circ_test_success) {
1089 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1090 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1093 if($self->circ_matrix_matchpoint) {
1094 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1095 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1096 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1097 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1099 my $policy = $self->get_circ_policy(
1100 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1102 $$results{$_} = $$policy{$_} for keys %$policy;
1108 # ---------------------------------------------------------------------
1109 # Loads the circ policy info for duration, recurring fine, and max
1110 # fine based on the current copy
1111 # ---------------------------------------------------------------------
1112 sub get_circ_policy {
1113 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1116 duration_rule => $duration_rule->name,
1117 recurring_fine_rule => $recurring_fine_rule->name,
1118 max_fine_rule => $max_fine_rule->name,
1119 max_fine => $self->get_max_fine_amount($max_fine_rule),
1120 fine_interval => $recurring_fine_rule->recurrence_interval,
1121 renewal_remaining => $duration_rule->max_renewals,
1122 grace_period => $recurring_fine_rule->grace_period
1125 if($hard_due_date) {
1126 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1127 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1130 $policy->{duration_date_ceiling} = undef;
1131 $policy->{duration_date_ceiling_force} = undef;
1134 $policy->{duration} = $duration_rule->shrt
1135 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1136 $policy->{duration} = $duration_rule->normal
1137 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1138 $policy->{duration} = $duration_rule->extended
1139 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1141 $policy->{recurring_fine} = $recurring_fine_rule->low
1142 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1143 $policy->{recurring_fine} = $recurring_fine_rule->normal
1144 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1145 $policy->{recurring_fine} = $recurring_fine_rule->high
1146 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1151 sub get_max_fine_amount {
1153 my $max_fine_rule = shift;
1154 my $max_amount = $max_fine_rule->amount;
1156 # if is_percent is true then the max->amount is
1157 # use as a percentage of the copy price
1158 if ($U->is_true($max_fine_rule->is_percent)) {
1159 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1160 $max_amount = $price * $max_fine_rule->amount / 100;
1162 $U->ou_ancestor_setting_value(
1164 'circ.max_fine.cap_at_price',
1168 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1169 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1177 sub run_copy_permit_scripts {
1179 my $copy = $self->copy || return;
1180 my $runner = $self->script_runner;
1184 my $results = $self->run_indb_circ_test;
1185 push @allevents, $self->matrix_test_result_events
1186 unless $self->circ_test_success;
1188 # See if this copy has an alert message
1189 my $ae = $self->check_copy_alert();
1190 push( @allevents, $ae ) if $ae;
1192 # uniquify the events
1193 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1194 @allevents = values %hash;
1196 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1198 $self->push_events(@allevents);
1202 sub check_copy_alert {
1204 return undef if $self->is_renewal;
1205 return OpenILS::Event->new(
1206 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1207 if $self->copy and $self->copy->alert_message;
1213 # --------------------------------------------------------------------------
1214 # If the call is overriding and has permissions to override every collected
1215 # event, the are cleared. Any event that the caller does not have
1216 # permission to override, will be left in the event list and bail_out will
1218 # XXX We need code in here to cancel any holds/transits on copies
1219 # that are being force-checked out
1220 # --------------------------------------------------------------------------
1221 sub override_events {
1223 my @events = @{$self->events};
1224 return unless @events;
1225 my $oargs = $self->override_args;
1227 if(!$self->override) {
1228 return $self->bail_out(1)
1229 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1234 for my $e (@events) {
1235 my $tc = $e->{textcode};
1236 next if $tc eq 'SUCCESS';
1237 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1238 my $ov = "$tc.override";
1239 $logger->info("circulator: attempting to override event: $ov");
1241 return $self->bail_on_events($self->editor->event)
1242 unless( $self->editor->allowed($ov) );
1244 return $self->bail_out(1);
1250 # --------------------------------------------------------------------------
1251 # If there is an open claimsreturn circ on the requested copy, close the
1252 # circ if overriding, otherwise bail out
1253 # --------------------------------------------------------------------------
1254 sub handle_claims_returned {
1256 my $copy = $self->copy;
1258 my $CR = $self->editor->search_action_circulation(
1260 target_copy => $copy->id,
1261 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1262 checkin_time => undef,
1266 return unless ($CR = $CR->[0]);
1270 # - If the caller has set the override flag, we will check the item in
1271 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1273 $CR->checkin_time('now');
1274 $CR->checkin_scan_time('now');
1275 $CR->checkin_lib($self->circ_lib);
1276 $CR->checkin_workstation($self->editor->requestor->wsid);
1277 $CR->checkin_staff($self->editor->requestor->id);
1279 $evt = $self->editor->event
1280 unless $self->editor->update_action_circulation($CR);
1283 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1286 $self->bail_on_events($evt) if $evt;
1291 # --------------------------------------------------------------------------
1292 # This performs the checkout
1293 # --------------------------------------------------------------------------
1297 $self->log_me("do_checkout()");
1299 # make sure perms are good if this isn't a renewal
1300 unless( $self->is_renewal ) {
1301 return $self->bail_on_events($self->editor->event)
1302 unless( $self->editor->allowed('COPY_CHECKOUT') );
1305 # verify the permit key
1306 unless( $self->check_permit_key ) {
1307 if( $self->permit_override ) {
1308 return $self->bail_on_events($self->editor->event)
1309 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1311 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1315 # if this is a non-cataloged circ, build the circ and finish
1316 if( $self->is_noncat ) {
1317 $self->checkout_noncat;
1319 OpenILS::Event->new('SUCCESS',
1320 payload => { noncat_circ => $self->circ }));
1324 if( $self->is_precat ) {
1325 $self->make_precat_copy;
1326 return if $self->bail_out;
1328 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1329 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1332 $self->do_copy_checks;
1333 return if $self->bail_out;
1335 $self->run_checkout_scripts();
1336 return if $self->bail_out;
1338 $self->build_checkout_circ_object();
1339 return if $self->bail_out;
1341 my $modify_to_start = $self->booking_adjusted_due_date();
1342 return if $self->bail_out;
1344 $self->apply_modified_due_date($modify_to_start);
1345 return if $self->bail_out;
1347 return $self->bail_on_events($self->editor->event)
1348 unless $self->editor->create_action_circulation($self->circ);
1350 # refresh the circ to force local time zone for now
1351 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1353 if($self->limit_groups) {
1354 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1357 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1359 return if $self->bail_out;
1361 $self->apply_deposit_fee();
1362 return if $self->bail_out;
1364 $self->handle_checkout_holds();
1365 return if $self->bail_out;
1367 # ------------------------------------------------------------------------------
1368 # Update the patron penalty info in the DB. Run it for permit-overrides
1369 # since the penalties are not updated during the permit phase
1370 # ------------------------------------------------------------------------------
1371 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1373 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1376 if($self->is_renewal) {
1377 # flesh the billing summary for the checked-in circ
1378 $pcirc = $self->editor->retrieve_action_circulation([
1380 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1385 OpenILS::Event->new('SUCCESS',
1387 copy => $U->unflesh_copy($self->copy),
1388 volume => $self->volume,
1389 circ => $self->circ,
1391 holds_fulfilled => $self->fulfilled_holds,
1392 deposit_billing => $self->deposit_billing,
1393 rental_billing => $self->rental_billing,
1394 parent_circ => $pcirc,
1395 patron => ($self->return_patron) ? $self->patron : undef,
1396 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1402 sub apply_deposit_fee {
1404 my $copy = $self->copy;
1406 ($self->is_deposit and not $self->is_deposit_exempt) or
1407 ($self->is_rental and not $self->is_rental_exempt);
1409 return if $self->is_deposit and $self->skip_deposit_fee;
1410 return if $self->is_rental and $self->skip_rental_fee;
1412 my $bill = Fieldmapper::money::billing->new;
1413 my $amount = $copy->deposit_amount;
1417 if($self->is_deposit) {
1418 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1420 $self->deposit_billing($bill);
1422 $billing_type = OILS_BILLING_TYPE_RENTAL;
1424 $self->rental_billing($bill);
1427 $bill->xact($self->circ->id);
1428 $bill->amount($amount);
1429 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1430 $bill->billing_type($billing_type);
1431 $bill->btype($btype);
1432 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1434 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1439 my $copy = $self->copy;
1441 my $stat = $copy->status if ref $copy->status;
1442 my $loc = $copy->location if ref $copy->location;
1443 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1445 $copy->status($stat->id) if $stat;
1446 $copy->location($loc->id) if $loc;
1447 $copy->circ_lib($circ_lib->id) if $circ_lib;
1448 $copy->editor($self->editor->requestor->id);
1449 $copy->edit_date('now');
1450 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1452 return $self->bail_on_events($self->editor->event)
1453 unless $self->editor->update_asset_copy($self->copy);
1455 $copy->status($U->copy_status($copy->status));
1456 $copy->location($loc) if $loc;
1457 $copy->circ_lib($circ_lib) if $circ_lib;
1460 sub update_reservation {
1462 my $reservation = $self->reservation;
1464 my $usr = $reservation->usr;
1465 my $target_rt = $reservation->target_resource_type;
1466 my $target_r = $reservation->target_resource;
1467 my $current_r = $reservation->current_resource;
1469 $reservation->usr($usr->id) if ref $usr;
1470 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1471 $reservation->target_resource($target_r->id) if ref $target_r;
1472 $reservation->current_resource($current_r->id) if ref $current_r;
1474 return $self->bail_on_events($self->editor->event)
1475 unless $self->editor->update_booking_reservation($self->reservation);
1478 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1479 $self->reservation($reservation);
1483 sub bail_on_events {
1484 my( $self, @evts ) = @_;
1485 $self->push_events(@evts);
1489 # ------------------------------------------------------------------------------
1490 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1491 # affects copies that will fulfill holds and CIRC affects all other copies.
1492 # If blocks exists, bail, push Events onto the event pile, and return true.
1493 # ------------------------------------------------------------------------------
1494 sub check_hold_fulfill_blocks {
1497 # See if the user has any penalties applied that prevent hold fulfillment
1498 my $pens = $self->editor->json_query({
1499 select => {csp => ['name', 'label']},
1500 from => {ausp => {csp => {}}},
1503 usr => $self->patron->id,
1504 org_unit => $U->get_org_full_path($self->circ_lib),
1506 {stop_date => undef},
1507 {stop_date => {'>' => 'now'}}
1510 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1514 return 0 unless @$pens;
1516 for my $pen (@$pens) {
1517 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1518 my $event = OpenILS::Event->new($pen->{name});
1519 $event->{desc} = $pen->{label};
1520 $self->push_events($event);
1523 $self->override_events;
1524 return $self->bail_out;
1528 # ------------------------------------------------------------------------------
1529 # When an item is checked out, see if we can fulfill a hold for this patron
1530 # ------------------------------------------------------------------------------
1531 sub handle_checkout_holds {
1533 my $copy = $self->copy;
1534 my $patron = $self->patron;
1536 my $e = $self->editor;
1537 $self->fulfilled_holds([]);
1539 # non-cats can't fulfill a hold
1540 return if $self->is_noncat;
1542 my $hold = $e->search_action_hold_request({
1543 current_copy => $copy->id ,
1544 cancel_time => undef,
1545 fulfillment_time => undef,
1547 {expire_time => undef},
1548 {expire_time => {'>' => 'now'}}
1552 if($hold and $hold->usr != $patron->id) {
1553 # reset the hold since the copy is now checked out
1555 $logger->info("circulator: un-targeting hold ".$hold->id.
1556 " because copy ".$copy->id." is getting checked out");
1558 $hold->clear_prev_check_time;
1559 $hold->clear_current_copy;
1560 $hold->clear_capture_time;
1561 $hold->clear_shelf_time;
1562 $hold->clear_shelf_expire_time;
1563 $hold->clear_current_shelf_lib;
1565 return $self->bail_on_event($e->event)
1566 unless $e->update_action_hold_request($hold);
1572 $hold = $self->find_related_user_hold($copy, $patron) or return;
1573 $logger->info("circulator: found related hold to fulfill in checkout");
1576 return if $self->check_hold_fulfill_blocks;
1578 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1580 # if the hold was never officially captured, capture it.
1581 $hold->current_copy($copy->id);
1582 $hold->capture_time('now') unless $hold->capture_time;
1583 $hold->fulfillment_time('now');
1584 $hold->fulfillment_staff($e->requestor->id);
1585 $hold->fulfillment_lib($self->circ_lib);
1587 return $self->bail_on_events($e->event)
1588 unless $e->update_action_hold_request($hold);
1590 return $self->fulfilled_holds([$hold->id]);
1594 # ------------------------------------------------------------------------------
1595 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1596 # the patron directly targets the checked out item, see if there is another hold
1597 # for the patron that could be fulfilled by the checked out item. Fulfill the
1598 # oldest hold and only fulfill 1 of them.
1600 # For "another hold":
1602 # First, check for one that the copy matches via hold_copy_map, ensuring that
1603 # *any* hold type that this copy could fill may end up filled.
1605 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1606 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1607 # that are non-requestable to count as capturing those hold types.
1608 # ------------------------------------------------------------------------------
1609 sub find_related_user_hold {
1610 my($self, $copy, $patron) = @_;
1611 my $e = $self->editor;
1613 # holds on precat copies are always copy-level, so this call will
1614 # always return undef. Exit early.
1615 return undef if $self->is_precat;
1617 return undef unless $U->ou_ancestor_setting_value(
1618 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1620 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1622 select => {ahr => ['id']},
1631 fkey => 'current_copy',
1632 type => 'left' # there may be no current_copy
1639 fulfillment_time => undef,
1640 cancel_time => undef,
1642 {expire_time => undef},
1643 {expire_time => {'>' => 'now'}}
1647 target_copy => $self->copy->id
1651 {id => undef}, # left-join copy may be nonexistent
1652 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1656 order_by => {ahr => {request_time => {direction => 'asc'}}},
1660 my $hold_info = $e->json_query($args)->[0];
1661 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1662 return undef if $U->ou_ancestor_setting_value(
1663 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1665 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1667 select => {ahr => ['id']},
1672 fkey => 'current_copy',
1673 type => 'left' # there may be no current_copy
1680 fulfillment_time => undef,
1681 cancel_time => undef,
1683 {expire_time => undef},
1684 {expire_time => {'>' => 'now'}}
1691 target => $self->volume->id
1697 target => $self->title->id
1703 {id => undef}, # left-join copy may be nonexistent
1704 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1708 order_by => {ahr => {request_time => {direction => 'asc'}}},
1712 $hold_info = $e->json_query($args)->[0];
1713 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1718 sub run_checkout_scripts {
1723 my $runner = $self->script_runner;
1732 my $hard_due_date_name;
1734 $self->run_indb_circ_test();
1735 $duration = $self->circ_matrix_matchpoint->duration_rule;
1736 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1737 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1738 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1740 $duration_name = $duration->name if $duration;
1741 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1744 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1745 return $self->bail_on_events($evt) if ($evt && !$nobail);
1747 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1748 return $self->bail_on_events($evt) if ($evt && !$nobail);
1750 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1751 return $self->bail_on_events($evt) if ($evt && !$nobail);
1753 if($hard_due_date_name) {
1754 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1755 return $self->bail_on_events($evt) if ($evt && !$nobail);
1761 # The item circulates with an unlimited duration
1765 $hard_due_date = undef;
1768 $self->duration_rule($duration);
1769 $self->recurring_fines_rule($recurring);
1770 $self->max_fine_rule($max_fine);
1771 $self->hard_due_date($hard_due_date);
1775 sub build_checkout_circ_object {
1778 my $circ = Fieldmapper::action::circulation->new;
1779 my $duration = $self->duration_rule;
1780 my $max = $self->max_fine_rule;
1781 my $recurring = $self->recurring_fines_rule;
1782 my $hard_due_date = $self->hard_due_date;
1783 my $copy = $self->copy;
1784 my $patron = $self->patron;
1785 my $duration_date_ceiling;
1786 my $duration_date_ceiling_force;
1790 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1791 $duration_date_ceiling = $policy->{duration_date_ceiling};
1792 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1794 my $dname = $duration->name;
1795 my $mname = $max->name;
1796 my $rname = $recurring->name;
1798 if($hard_due_date) {
1799 $hdname = $hard_due_date->name;
1802 $logger->debug("circulator: building circulation ".
1803 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1805 $circ->duration($policy->{duration});
1806 $circ->recurring_fine($policy->{recurring_fine});
1807 $circ->duration_rule($duration->name);
1808 $circ->recurring_fine_rule($recurring->name);
1809 $circ->max_fine_rule($max->name);
1810 $circ->max_fine($policy->{max_fine});
1811 $circ->fine_interval($recurring->recurrence_interval);
1812 $circ->renewal_remaining($duration->max_renewals);
1813 $circ->grace_period($policy->{grace_period});
1817 $logger->info("circulator: copy found with an unlimited circ duration");
1818 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1819 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1820 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1821 $circ->renewal_remaining(0);
1822 $circ->grace_period(0);
1825 $circ->target_copy( $copy->id );
1826 $circ->usr( $patron->id );
1827 $circ->circ_lib( $self->circ_lib );
1828 $circ->workstation($self->editor->requestor->wsid)
1829 if defined $self->editor->requestor->wsid;
1831 # renewals maintain a link to the parent circulation
1832 $circ->parent_circ($self->parent_circ);
1834 if( $self->is_renewal ) {
1835 $circ->opac_renewal('t') if $self->opac_renewal;
1836 $circ->phone_renewal('t') if $self->phone_renewal;
1837 $circ->desk_renewal('t') if $self->desk_renewal;
1838 $circ->renewal_remaining($self->renewal_remaining);
1839 $circ->circ_staff($self->editor->requestor->id);
1843 # if the user provided an overiding checkout time,
1844 # (e.g. the checkout really happened several hours ago), then
1845 # we apply that here. Does this need a perm??
1846 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1847 if $self->checkout_time;
1849 # if a patron is renewing, 'requestor' will be the patron
1850 $circ->circ_staff($self->editor->requestor->id);
1851 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
1856 sub do_reservation_pickup {
1859 $self->log_me("do_reservation_pickup()");
1861 $self->reservation->pickup_time('now');
1864 $self->reservation->current_resource &&
1865 $U->is_true($self->reservation->target_resource_type->catalog_item)
1867 # We used to try to set $self->copy and $self->patron here,
1868 # but that should already be done.
1870 $self->run_checkout_scripts(1);
1872 my $duration = $self->duration_rule;
1873 my $max = $self->max_fine_rule;
1874 my $recurring = $self->recurring_fines_rule;
1876 if ($duration && $max && $recurring) {
1877 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1879 my $dname = $duration->name;
1880 my $mname = $max->name;
1881 my $rname = $recurring->name;
1883 $logger->debug("circulator: updating reservation ".
1884 "with duration=$dname, maxfine=$mname, recurring=$rname");
1886 $self->reservation->fine_amount($policy->{recurring_fine});
1887 $self->reservation->max_fine($policy->{max_fine});
1888 $self->reservation->fine_interval($recurring->recurrence_interval);
1891 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1892 $self->update_copy();
1895 $self->reservation->fine_amount(
1896 $self->reservation->target_resource_type->fine_amount
1898 $self->reservation->max_fine(
1899 $self->reservation->target_resource_type->max_fine
1901 $self->reservation->fine_interval(
1902 $self->reservation->target_resource_type->fine_interval
1906 $self->update_reservation();
1909 sub do_reservation_return {
1911 my $request = shift;
1913 $self->log_me("do_reservation_return()");
1915 if (not ref $self->reservation) {
1916 my ($reservation, $evt) =
1917 $U->fetch_booking_reservation($self->reservation);
1918 return $self->bail_on_events($evt) if $evt;
1919 $self->reservation($reservation);
1922 $self->handle_fines(1);
1923 $self->reservation->return_time('now');
1924 $self->update_reservation();
1925 $self->reshelve_copy if $self->copy;
1927 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1928 $self->copy( $self->reservation->current_resource->catalog_item );
1932 sub booking_adjusted_due_date {
1934 my $circ = $self->circ;
1935 my $copy = $self->copy;
1937 return undef unless $self->use_booking;
1941 if( $self->due_date ) {
1943 return $self->bail_on_events($self->editor->event)
1944 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1946 $circ->due_date(cleanse_ISO8601($self->due_date));
1950 return unless $copy and $circ->due_date;
1953 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1954 if (@$booking_items) {
1955 my $booking_item = $booking_items->[0];
1956 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1958 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1959 my $shorten_circ_setting = $resource_type->elbow_room ||
1960 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1963 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1964 my $bookings = $booking_ses->request(
1965 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1966 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
1968 $booking_ses->disconnect;
1970 my $dt_parser = DateTime::Format::ISO8601->new;
1971 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1973 for my $bid (@$bookings) {
1975 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1977 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1978 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1980 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1981 if ($booking_start < DateTime->now);
1984 if ($U->is_true($stop_circ_setting)) {
1985 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
1987 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
1988 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
1991 # We set the circ duration here only to affect the logic that will
1992 # later (in a DB trigger) mangle the time part of the due date to
1993 # 11:59pm. Having any circ duration that is not a whole number of
1994 # days is enough to prevent the "correction."
1995 my $new_circ_duration = $due_date->epoch - time;
1996 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
1997 $circ->duration("$new_circ_duration seconds");
1999 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2003 return $self->bail_on_events($self->editor->event)
2004 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2010 sub apply_modified_due_date {
2012 my $shift_earlier = shift;
2013 my $circ = $self->circ;
2014 my $copy = $self->copy;
2016 if( $self->due_date ) {
2018 return $self->bail_on_events($self->editor->event)
2019 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2021 $circ->due_date(cleanse_ISO8601($self->due_date));
2025 # if the due_date lands on a day when the location is closed
2026 return unless $copy and $circ->due_date;
2028 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2030 # due-date overlap should be determined by the location the item
2031 # is checked out from, not the owning or circ lib of the item
2032 my $org = $self->circ_lib;
2034 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2035 " with an item due date of ".$circ->due_date );
2037 my $dateinfo = $U->storagereq(
2038 'open-ils.storage.actor.org_unit.closed_date.overlap',
2039 $org, $circ->due_date );
2042 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2043 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2045 # XXX make the behavior more dynamic
2046 # for now, we just push the due date to after the close date
2047 if ($shift_earlier) {
2048 $circ->due_date($dateinfo->{start});
2050 $circ->due_date($dateinfo->{end});
2058 sub create_due_date {
2059 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2061 # if there is a raw time component (e.g. from postgres),
2062 # turn it into an interval that interval_to_seconds can parse
2063 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2065 # for now, use the server timezone. TODO: use workstation org timezone
2066 my $due_date = DateTime->now(time_zone => 'local');
2067 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2069 # add the circ duration
2070 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2073 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2074 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2075 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2080 # return ISO8601 time with timezone
2081 return $due_date->strftime('%FT%T%z');
2086 sub make_precat_copy {
2088 my $copy = $self->copy;
2091 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2093 $copy->editor($self->editor->requestor->id);
2094 $copy->edit_date('now');
2095 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2096 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2097 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2098 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2099 $self->update_copy();
2103 $logger->info("circulator: Creating a new precataloged ".
2104 "copy in checkout with barcode " . $self->copy_barcode);
2106 $copy = Fieldmapper::asset::copy->new;
2107 $copy->circ_lib($self->circ_lib);
2108 $copy->creator($self->editor->requestor->id);
2109 $copy->editor($self->editor->requestor->id);
2110 $copy->barcode($self->copy_barcode);
2111 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2112 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2113 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2115 $copy->dummy_title($self->dummy_title || "");
2116 $copy->dummy_author($self->dummy_author || "");
2117 $copy->dummy_isbn($self->dummy_isbn || "");
2118 $copy->circ_modifier($self->circ_modifier);
2121 # See if we need to override the circ_lib for the copy with a configured circ_lib
2122 # Setting is shortname of the org unit
2123 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2124 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2126 if($precat_circ_lib) {
2127 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2130 $self->bail_on_events($self->editor->event);
2134 $copy->circ_lib($org->id);
2138 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2140 $self->push_events($self->editor->event);
2144 # this is a little bit of a hack, but we need to
2145 # get the copy into the script runner
2146 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2150 sub checkout_noncat {
2156 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2157 my $count = $self->noncat_count || 1;
2158 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2160 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2164 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2165 $self->editor->requestor->id,
2173 $self->push_events($evt);
2181 # If a copy goes into transit and is then checked in before the transit checkin
2182 # interval has expired, push an event onto the overridable events list.
2183 sub check_transit_checkin_interval {
2186 # only concerned with in-transit items
2187 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2189 # no interval, no problem
2190 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2191 return unless $interval;
2193 # capture the transit so we don't have to fetch it again later during checkin
2195 $self->editor->search_action_transit_copy(
2196 {target_copy => $self->copy->id, dest_recv_time => undef}
2200 # transit from X to X for whatever reason has no min interval
2201 return if $self->transit->source == $self->transit->dest;
2203 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2204 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2205 my $horizon = $t_start->add(seconds => $seconds);
2207 # See if we are still within the transit checkin forbidden range
2208 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2209 if $horizon > DateTime->now;
2212 # Retarget local holds at checkin
2213 sub checkin_retarget {
2215 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2216 return unless $self->is_checkin; # Renewals need not be checked
2217 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2218 return if $self->is_precat; # No holds for precats
2219 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2220 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2221 my $status = $U->copy_status($self->copy->status);
2222 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2223 # Specifically target items that are likely new (by status ID)
2224 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2225 my $location = $self->copy->location;
2226 if(!ref($location)) {
2227 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2228 $self->copy->location($location);
2230 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2232 # Fetch holds for the bib
2233 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2234 $self->editor->authtoken,
2237 capture_time => undef, # No touching captured holds
2238 frozen => 'f', # Don't bother with frozen holds
2239 pickup_lib => $self->circ_lib # Only holds actually here
2242 # Error? Skip the step.
2243 return if exists $result->{"ilsevent"};
2247 foreach my $holdlist (keys %{$result}) {
2248 push @$holds, @{$result->{$holdlist}};
2251 return if scalar(@$holds) == 0; # No holds, no retargeting
2253 # Check for parts on this copy
2254 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2255 my %parts_hash = ();
2256 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2258 # Loop over holds in request-ish order
2259 # Stage 1: Get them into request-ish order
2260 # Also grab type and target for skipping low hanging ones
2261 $result = $self->editor->json_query({
2262 "select" => { "ahr" => ["id", "hold_type", "target"] },
2263 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2264 "where" => { "id" => $holds },
2266 { "class" => "pgt", "field" => "hold_priority"},
2267 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2268 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2269 { "class" => "ahr", "field" => "request_time"}
2274 if (ref $result eq "ARRAY" and scalar @$result) {
2275 foreach (@{$result}) {
2276 # Copy level, but not this copy?
2277 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2278 and $_->{target} != $self->copy->id);
2279 # Volume level, but not this volume?
2280 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2281 if(@$parts) { # We have parts?
2283 next if ($_->{hold_type} eq 'T');
2284 # Skip part holds for parts not on this copy
2285 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2287 # No parts, no part holds
2288 next if ($_->{hold_type} eq 'P');
2290 # So much for easy stuff, attempt a retarget!
2291 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2292 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2293 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2301 $self->log_me("do_checkin()");
2303 return $self->bail_on_events(
2304 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2307 $self->check_transit_checkin_interval;
2308 $self->checkin_retarget;
2310 # the renew code and mk_env should have already found our circulation object
2311 unless( $self->circ ) {
2313 my $circs = $self->editor->search_action_circulation(
2314 { target_copy => $self->copy->id, checkin_time => undef });
2316 $self->circ($$circs[0]);
2318 # for now, just warn if there are multiple open circs on a copy
2319 $logger->warn("circulator: we have ".scalar(@$circs).
2320 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2323 my $stat = $U->copy_status($self->copy->status)->id;
2325 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2326 # differently if they are already paid for. We need to check for this
2327 # early since overdue generation is potentially affected.
2328 my $dont_change_lost_zero = 0;
2329 if ($stat == OILS_COPY_STATUS_LOST
2330 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2331 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2333 # LOST fine settings are controlled by the copy's circ lib, not the the
2335 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2336 $self->copy->circ_lib->id : $self->copy->circ_lib;
2337 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2338 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2339 $self->editor) || 0;
2341 if ($dont_change_lost_zero) {
2342 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2343 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2346 $self->dont_change_lost_zero($dont_change_lost_zero);
2349 if( $self->checkin_check_holds_shelf() ) {
2350 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2351 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2352 if($self->fake_hold_dest) {
2353 $self->hold->pickup_lib($self->circ_lib);
2355 $self->checkin_flesh_events;
2359 unless( $self->is_renewal ) {
2360 return $self->bail_on_events($self->editor->event)
2361 unless $self->editor->allowed('COPY_CHECKIN');
2364 $self->push_events($self->check_copy_alert());
2365 $self->push_events($self->check_checkin_copy_status());
2367 # if the circ is marked as 'claims returned', add the event to the list
2368 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2369 if ($self->circ and $self->circ->stop_fines
2370 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2372 $self->check_circ_deposit();
2374 # handle the overridable events
2375 $self->override_events unless $self->is_renewal;
2376 return if $self->bail_out;
2378 if( $self->copy and !$self->transit ) {
2380 $self->editor->search_action_transit_copy(
2381 { target_copy => $self->copy->id, dest_recv_time => undef }
2387 $self->checkin_handle_circ;
2388 return if $self->bail_out;
2389 $self->checkin_changed(1);
2391 if (!$dont_change_lost_zero) {
2392 # if this circ is LOST and we are configured to generate overdue
2393 # fines for lost items on checkin (to fill the gap between mark
2394 # lost time and when the fines would have naturally stopped), then
2395 # stop_fines is no longer valid and should be cleared.
2397 # stop_fines will be set again during the handle_fines() stage.
2398 # XXX should this setting come from the copy circ lib (like other
2399 # LOST settings), instead of the circulation circ lib?
2400 if ($stat == OILS_COPY_STATUS_LOST) {
2401 $self->circ->clear_stop_fines if
2402 $U->ou_ancestor_setting_value(
2404 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2409 # handle fines for this circ, including overdue gen if needed
2410 $self->handle_fines;
2412 } elsif( $self->transit ) {
2413 my $hold_transit = $self->process_received_transit;
2414 $self->checkin_changed(1);
2416 if( $self->bail_out ) {
2417 $self->checkin_flesh_events;
2421 if( my $e = $self->check_checkin_copy_status() ) {
2422 # If the original copy status is special, alert the caller
2423 my $ev = $self->events;
2424 $self->events([$e]);
2425 $self->override_events;
2426 return if $self->bail_out;
2430 if( $hold_transit or
2431 $U->copy_status($self->copy->status)->id
2432 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2435 if( $hold_transit ) {
2436 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2438 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2443 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2445 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2446 $self->reshelve_copy(1);
2447 $self->cancelled_hold_transit(1);
2448 $self->notify_hold(0); # don't notify for cancelled holds
2449 $self->fake_hold_dest(0);
2450 return if $self->bail_out;
2452 } elsif ($hold and $hold->hold_type eq 'R') {
2454 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2455 $self->notify_hold(0); # No need to notify
2456 $self->fake_hold_dest(0);
2457 $self->noop(1); # Don't try and capture for other holds/transits now
2458 $self->update_copy();
2459 $hold->fulfillment_time('now');
2460 $self->bail_on_events($self->editor->event)
2461 unless $self->editor->update_action_hold_request($hold);
2465 # hold transited to correct location
2466 if($self->fake_hold_dest) {
2467 $hold->pickup_lib($self->circ_lib);
2469 $self->checkin_flesh_events;
2474 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2476 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2477 " that is in-transit, but there is no transit.. repairing");
2478 $self->reshelve_copy(1);
2479 return if $self->bail_out;
2482 if( $self->is_renewal ) {
2483 $self->finish_fines_and_voiding;
2484 return if $self->bail_out;
2485 $self->push_events(OpenILS::Event->new('SUCCESS'));
2489 # ------------------------------------------------------------------------------
2490 # Circulations and transits are now closed where necessary. Now go on to see if
2491 # this copy can fulfill a hold or needs to be routed to a different location
2492 # ------------------------------------------------------------------------------
2494 my $needed_for_something = 0; # formerly "needed_for_hold"
2496 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2498 if (!$self->remote_hold) {
2499 if ($self->use_booking) {
2500 my $potential_hold = $self->hold_capture_is_possible;
2501 my $potential_reservation = $self->reservation_capture_is_possible;
2503 if ($potential_hold and $potential_reservation) {
2504 $logger->info("circulator: item could fulfill either hold or reservation");
2505 $self->push_events(new OpenILS::Event(
2506 "HOLD_RESERVATION_CONFLICT",
2507 "hold" => $potential_hold,
2508 "reservation" => $potential_reservation
2510 return if $self->bail_out;
2511 } elsif ($potential_hold) {
2512 $needed_for_something =
2513 $self->attempt_checkin_hold_capture;
2514 } elsif ($potential_reservation) {
2515 $needed_for_something =
2516 $self->attempt_checkin_reservation_capture;
2519 $needed_for_something = $self->attempt_checkin_hold_capture;
2522 return if $self->bail_out;
2524 unless($needed_for_something) {
2525 my $circ_lib = (ref $self->copy->circ_lib) ?
2526 $self->copy->circ_lib->id : $self->copy->circ_lib;
2528 if( $self->remote_hold ) {
2529 $circ_lib = $self->remote_hold->pickup_lib;
2530 $logger->warn("circulator: Copy ".$self->copy->barcode.
2531 " is on a remote hold's shelf, sending to $circ_lib");
2534 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2536 my $suppress_transit = 0;
2538 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2539 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2540 if($suppress_transit_source && $suppress_transit_source->{value}) {
2541 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2542 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2543 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2544 $suppress_transit = 1;
2549 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2550 # copy is where it needs to be, either for hold or reshelving
2552 $self->checkin_handle_precat();
2553 return if $self->bail_out;
2556 # copy needs to transit "home", or stick here if it's a floating copy
2558 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2559 my $res = $self->editor->json_query(
2561 'evergreen.can_float',
2562 $self->copy->floating->id,
2563 $self->copy->circ_lib,
2568 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2570 if ($can_float) { # Yep, floating, stick here
2571 $self->checkin_changed(1);
2572 $self->copy->circ_lib( $self->circ_lib );
2575 my $bc = $self->copy->barcode;
2576 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2577 $self->checkin_build_copy_transit($circ_lib);
2578 return if $self->bail_out;
2579 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2583 } else { # no-op checkin
2584 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2585 $self->checkin_changed(1);
2586 $self->copy->circ_lib( $self->circ_lib );
2591 if($self->claims_never_checked_out and
2592 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2594 # the item was not supposed to be checked out to the user and should now be marked as missing
2595 $self->copy->status(OILS_COPY_STATUS_MISSING);
2599 $self->reshelve_copy unless $needed_for_something;
2602 return if $self->bail_out;
2604 unless($self->checkin_changed) {
2606 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2607 my $stat = $U->copy_status($self->copy->status)->id;
2609 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2610 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2611 $self->bail_out(1); # no need to commit anything
2615 $self->push_events(OpenILS::Event->new('SUCCESS'))
2616 unless @{$self->events};
2619 $self->finish_fines_and_voiding;
2621 OpenILS::Utils::Penalty->calculate_penalties(
2622 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2624 $self->checkin_flesh_events;
2628 sub finish_fines_and_voiding {
2630 return unless $self->circ;
2632 return unless $self->backdate or $self->void_overdues;
2634 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2635 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2637 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2638 $self->editor, $self->circ, $self->backdate, $note);
2640 return $self->bail_on_events($evt) if $evt;
2642 # Make sure the circ is open or closed as necessary.
2643 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2644 return $self->bail_on_events($evt) if $evt;
2650 # if a deposit was payed for this item, push the event
2651 sub check_circ_deposit {
2653 return unless $self->circ;
2654 my $deposit = $self->editor->search_money_billing(
2656 xact => $self->circ->id,
2658 }, {idlist => 1})->[0];
2660 $self->push_events(OpenILS::Event->new(
2661 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2666 my $force = $self->force || shift;
2667 my $copy = $self->copy;
2669 my $stat = $U->copy_status($copy->status)->id;
2672 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2673 $stat != OILS_COPY_STATUS_CATALOGING and
2674 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2675 $stat != OILS_COPY_STATUS_RESHELVING )) {
2677 $copy->status( OILS_COPY_STATUS_RESHELVING );
2679 $self->checkin_changed(1);
2684 # Returns true if the item is at the current location
2685 # because it was transited there for a hold and the
2686 # hold has not been fulfilled
2687 sub checkin_check_holds_shelf {
2689 return 0 unless $self->copy;
2692 $U->copy_status($self->copy->status)->id ==
2693 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2695 # Attempt to clear shelf expired holds for this copy
2696 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2697 if($self->clear_expired);
2699 # find the hold that put us on the holds shelf
2700 my $holds = $self->editor->search_action_hold_request(
2702 current_copy => $self->copy->id,
2703 capture_time => { '!=' => undef },
2704 fulfillment_time => undef,
2705 cancel_time => undef,
2710 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2711 $self->reshelve_copy(1);
2715 my $hold = $$holds[0];
2717 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2718 $hold->id. "] for copy ".$self->copy->barcode);
2720 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2721 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2722 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2723 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2724 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2725 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2726 $self->fake_hold_dest(1);
2732 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2733 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2737 $logger->info("circulator: hold is not for here..");
2738 $self->remote_hold($hold);
2743 sub checkin_handle_precat {
2745 my $copy = $self->copy;
2747 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2748 $copy->status(OILS_COPY_STATUS_CATALOGING);
2749 $self->update_copy();
2750 $self->checkin_changed(1);
2751 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2756 sub checkin_build_copy_transit {
2759 my $copy = $self->copy;
2760 my $transit = Fieldmapper::action::transit_copy->new;
2762 # if we are transiting an item to the shelf shelf, it's a hold transit
2763 if (my $hold = $self->remote_hold) {
2764 $transit = Fieldmapper::action::hold_transit_copy->new;
2765 $transit->hold($hold->id);
2767 # the item is going into transit, remove any shelf-iness
2768 if ($hold->current_shelf_lib or $hold->shelf_time) {
2769 $hold->clear_current_shelf_lib;
2770 $hold->clear_shelf_time;
2771 return $self->bail_on_events($self->editor->event)
2772 unless $self->editor->update_action_hold_request($hold);
2776 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2777 $logger->info("circulator: transiting copy to $dest");
2779 $transit->source($self->circ_lib);
2780 $transit->dest($dest);
2781 $transit->target_copy($copy->id);
2782 $transit->source_send_time('now');
2783 $transit->copy_status( $U->copy_status($copy->status)->id );
2785 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2787 if ($self->remote_hold) {
2788 return $self->bail_on_events($self->editor->event)
2789 unless $self->editor->create_action_hold_transit_copy($transit);
2791 return $self->bail_on_events($self->editor->event)
2792 unless $self->editor->create_action_transit_copy($transit);
2795 # ensure the transit is returned to the caller
2796 $self->transit($transit);
2798 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2800 $self->checkin_changed(1);
2804 sub hold_capture_is_possible {
2806 my $copy = $self->copy;
2808 # we've been explicitly told not to capture any holds
2809 return 0 if $self->capture eq 'nocapture';
2811 # See if this copy can fulfill any holds
2812 my $hold = $holdcode->find_nearest_permitted_hold(
2813 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2815 return undef if ref $hold eq "HASH" and
2816 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2820 sub reservation_capture_is_possible {
2822 my $copy = $self->copy;
2824 # we've been explicitly told not to capture any holds
2825 return 0 if $self->capture eq 'nocapture';
2827 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2828 my $resv = $booking_ses->request(
2829 "open-ils.booking.reservations.could_capture",
2830 $self->editor->authtoken, $copy->barcode
2832 $booking_ses->disconnect;
2833 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2834 $self->push_events($resv);
2840 # returns true if the item was used (or may potentially be used
2841 # in subsequent calls) to capture a hold.
2842 sub attempt_checkin_hold_capture {
2844 my $copy = $self->copy;
2846 # we've been explicitly told not to capture any holds
2847 return 0 if $self->capture eq 'nocapture';
2849 # See if this copy can fulfill any holds
2850 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2851 $self->editor, $copy, $self->editor->requestor );
2854 $logger->debug("circulator: no potential permitted".
2855 "holds found for copy ".$copy->barcode);
2859 if($self->capture ne 'capture') {
2860 # see if this item is in a hold-capture-delay location
2861 my $location = $self->copy->location;
2862 if(!ref($location)) {
2863 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2864 $self->copy->location($location);
2866 if($U->is_true($location->hold_verify)) {
2867 $self->bail_on_events(
2868 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2873 $self->retarget($retarget);
2875 my $suppress_transit = 0;
2876 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2877 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2878 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2879 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2880 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2881 $suppress_transit = 1;
2882 $hold->pickup_lib($self->circ_lib);
2887 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2889 $hold->current_copy($copy->id);
2890 $hold->capture_time('now');
2891 $self->put_hold_on_shelf($hold)
2892 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2894 # prevent DB errors caused by fetching
2895 # holds from storage, and updating through cstore
2896 $hold->clear_fulfillment_time;
2897 $hold->clear_fulfillment_staff;
2898 $hold->clear_fulfillment_lib;
2899 $hold->clear_expire_time;
2900 $hold->clear_cancel_time;
2901 $hold->clear_prev_check_time unless $hold->prev_check_time;
2903 $self->bail_on_events($self->editor->event)
2904 unless $self->editor->update_action_hold_request($hold);
2906 $self->checkin_changed(1);
2908 return 0 if $self->bail_out;
2910 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2912 if ($hold->hold_type eq 'R') {
2913 $copy->status(OILS_COPY_STATUS_CATALOGING);
2914 $hold->fulfillment_time('now');
2915 $self->noop(1); # Block other transit/hold checks
2916 $self->bail_on_events($self->editor->event)
2917 unless $self->editor->update_action_hold_request($hold);
2919 # This hold was captured in the correct location
2920 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2921 $self->push_events(OpenILS::Event->new('SUCCESS'));
2923 #$self->do_hold_notify($hold->id);
2924 $self->notify_hold($hold->id);
2929 # Hold needs to be picked up elsewhere. Build a hold
2930 # transit and route the item.
2931 $self->checkin_build_hold_transit();
2932 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2933 return 0 if $self->bail_out;
2934 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2937 # make sure we save the copy status
2939 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
2943 sub attempt_checkin_reservation_capture {
2945 my $copy = $self->copy;
2947 # we've been explicitly told not to capture any holds
2948 return 0 if $self->capture eq 'nocapture';
2950 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2951 my $evt = $booking_ses->request(
2952 "open-ils.booking.resources.capture_for_reservation",
2953 $self->editor->authtoken,
2955 1 # don't update copy - we probably have it locked
2957 $booking_ses->disconnect;
2959 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2961 "open-ils.booking.resources.capture_for_reservation " .
2962 "didn't return an event!"
2966 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2967 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2969 # not-transferable is an error event we'll pass on the user
2970 $logger->warn("reservation capture attempted against non-transferable item");
2971 $self->push_events($evt);
2973 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2974 # Re-retrieve copy as reservation capture may have changed
2975 # its status and whatnot.
2977 "circulator: booking capture win on copy " . $self->copy->id
2979 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2981 "circulator: changing copy " . $self->copy->id .
2982 "'s status from " . $self->copy->status . " to " .
2985 $self->copy->status($new_copy_status);
2988 $self->reservation($evt->{"payload"}->{"reservation"});
2990 if (exists $evt->{"payload"}->{"transit"}) {
2994 "org" => $evt->{"payload"}->{"transit"}->dest
2998 $self->checkin_changed(1);
3002 # other results are treated as "nothing to capture"
3006 sub do_hold_notify {
3007 my( $self, $holdid ) = @_;
3009 my $e = new_editor(xact => 1);
3010 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3012 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3013 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3015 $logger->info("circulator: running delayed hold notify process");
3017 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3018 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3020 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3021 hold_id => $holdid, requestor => $self->editor->requestor);
3023 $logger->debug("circulator: built hold notifier");
3025 if(!$notifier->event) {
3027 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3029 my $stat = $notifier->send_email_notify;
3030 if( $stat == '1' ) {
3031 $logger->info("circulator: hold notify succeeded for hold $holdid");
3035 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3038 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3042 sub retarget_holds {
3044 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3045 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3046 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3047 # no reason to wait for the return value
3051 sub checkin_build_hold_transit {
3054 my $copy = $self->copy;
3055 my $hold = $self->hold;
3056 my $trans = Fieldmapper::action::hold_transit_copy->new;
3058 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3060 $trans->hold($hold->id);
3061 $trans->source($self->circ_lib);
3062 $trans->dest($hold->pickup_lib);
3063 $trans->source_send_time("now");
3064 $trans->target_copy($copy->id);
3066 # when the copy gets to its destination, it will recover
3067 # this status - put it onto the holds shelf
3068 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3070 return $self->bail_on_events($self->editor->event)
3071 unless $self->editor->create_action_hold_transit_copy($trans);
3076 sub process_received_transit {
3078 my $copy = $self->copy;
3079 my $copyid = $self->copy->id;
3081 my $status_name = $U->copy_status($copy->status)->name;
3082 $logger->debug("circulator: attempting transit receive on ".
3083 "copy $copyid. Copy status is $status_name");
3085 my $transit = $self->transit;
3087 # Check if we are in a transit suppress range
3088 my $suppress_transit = 0;
3089 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3090 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3091 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3092 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3093 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3094 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3095 $suppress_transit = 1;
3096 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3100 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3101 # - this item is in-transit to a different location
3102 # - Or we are capturing holds as transits, so why create a new transit?
3104 my $tid = $transit->id;
3105 my $loc = $self->circ_lib;
3106 my $dest = $transit->dest;
3108 $logger->info("circulator: Fowarding transit on copy which is destined ".
3109 "for a different location. transit=$tid, copy=$copyid, current ".
3110 "location=$loc, destination location=$dest");
3112 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3114 # grab the associated hold object if available
3115 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3116 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3118 return $self->bail_on_events($evt);
3121 # The transit is received, set the receive time
3122 $transit->dest_recv_time('now');
3123 $self->bail_on_events($self->editor->event)
3124 unless $self->editor->update_action_transit_copy($transit);
3126 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3128 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3129 $copy->status( $transit->copy_status );
3130 $self->update_copy();
3131 return if $self->bail_out;
3135 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3138 # hold has arrived at destination, set shelf time
3139 $self->put_hold_on_shelf($hold);
3140 $self->bail_on_events($self->editor->event)
3141 unless $self->editor->update_action_hold_request($hold);
3142 return if $self->bail_out;
3144 $self->notify_hold($hold_transit->hold);
3147 $hold_transit = undef;
3148 $self->cancelled_hold_transit(1);
3149 $self->reshelve_copy(1);
3150 $self->fake_hold_dest(0);
3155 OpenILS::Event->new(
3158 payload => { transit => $transit, holdtransit => $hold_transit } ));
3160 return $hold_transit;
3164 # ------------------------------------------------------------------
3165 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3166 # ------------------------------------------------------------------
3167 sub put_hold_on_shelf {
3168 my($self, $hold) = @_;
3169 $hold->shelf_time('now');
3170 $hold->current_shelf_lib($self->circ_lib);
3171 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3177 my $reservation = shift;
3178 my $dt_parser = DateTime::Format::ISO8601->new;
3180 my $obj = $reservation ? $self->reservation : $self->circ;
3182 my $lost_bill_opts = $self->lost_bill_options;
3183 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3184 # first, restore any voided overdues for lost, if needed
3185 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3186 my $restore_od = $U->ou_ancestor_setting_value(
3187 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3188 $self->editor) || 0;
3189 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3193 # next, handle normal overdue generation and apply stop_fines
3194 # XXX reservations don't have stop_fines
3195 # TODO revisit booking_reservation re: stop_fines support
3196 if ($reservation or !$obj->stop_fines) {
3199 # This is a crude check for whether we are in a grace period. The code
3200 # in generate_fines() does a more thorough job, so this exists solely
3201 # as a small optimization, and might be better off removed.
3203 # If we have a grace period
3204 if($obj->can('grace_period')) {
3205 # Parse out the due date
3206 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3207 # Add the grace period to the due date
3208 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3209 # Don't generate fines on circs still in grace period
3210 $skip_for_grace = $due_date > DateTime->now;
3212 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3213 unless $skip_for_grace;
3215 if (!$reservation and !$obj->stop_fines) {
3216 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3217 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3218 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3219 $obj->stop_fines_time('now');
3220 $obj->stop_fines_time($self->backdate) if $self->backdate;
3221 $self->editor->update_action_circulation($obj);
3225 # finally, handle voiding of lost item and processing fees
3226 if ($self->needs_lost_bill_handling) {
3227 my $void_cost = $U->ou_ancestor_setting_value(
3228 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3229 $self->editor) || 0;
3230 my $void_proc_fee = $U->ou_ancestor_setting_value(
3231 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3232 $self->editor) || 0;
3233 $self->checkin_handle_lost_or_lo_now_found(
3234 $lost_bill_opts->{void_cost_btype},
3235 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3236 $self->checkin_handle_lost_or_lo_now_found(
3237 $lost_bill_opts->{void_fee_btype},
3238 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3244 sub checkin_handle_circ {
3246 my $circ = $self->circ;
3247 my $copy = $self->copy;
3251 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3253 # backdate the circ if necessary
3254 if($self->backdate) {
3255 my $evt = $self->checkin_handle_backdate;
3256 return $self->bail_on_events($evt) if $evt;
3259 # Set the checkin vars since we have the item
3260 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3262 # capture the true scan time for back-dated checkins
3263 $circ->checkin_scan_time('now');
3265 $circ->checkin_staff($self->editor->requestor->id);
3266 $circ->checkin_lib($self->circ_lib);
3267 $circ->checkin_workstation($self->editor->requestor->wsid);
3269 my $circ_lib = (ref $self->copy->circ_lib) ?
3270 $self->copy->circ_lib->id : $self->copy->circ_lib;
3271 my $stat = $U->copy_status($self->copy->status)->id;
3273 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3274 # we will now handle lost fines, but the copy will retain its 'lost'
3275 # status if it needs to transit home unless lost_immediately_available
3278 # if we decide to also delay fine handling until the item arrives home,
3279 # we will need to call lost fine handling code both when checking items
3280 # in and also when receiving transits
3281 $self->checkin_handle_lost($circ_lib);
3282 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3283 # same process as above.
3284 $self->checkin_handle_long_overdue($circ_lib);
3285 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3286 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3288 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3293 # see if there are any fines owed on this circ. if not, close it
3294 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3295 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3297 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3299 return $self->bail_on_events($self->editor->event)
3300 unless $self->editor->update_action_circulation($circ);
3305 # ------------------------------------------------------------------
3306 # See if we need to void billings, etc. for lost checkin
3307 # ------------------------------------------------------------------
3308 sub checkin_handle_lost {
3310 my $circ_lib = shift;
3312 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3313 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3315 $self->lost_bill_options({
3316 circ_lib => $circ_lib,
3317 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3318 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3319 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3320 void_cost_btype => 3,
3324 return $self->checkin_handle_lost_or_longoverdue(
3325 circ_lib => $circ_lib,
3326 max_return => $max_return,
3327 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3328 ous_use_last_activity => undef # not supported for LOST checkin
3332 # ------------------------------------------------------------------
3333 # See if we need to void billings, etc. for long-overdue checkin
3334 # note: not using constants below since they serve little purpose
3335 # for single-use strings that are descriptive in their own right
3336 # and mostly just complicate debugging.
3337 # ------------------------------------------------------------------
3338 sub checkin_handle_long_overdue {
3340 my $circ_lib = shift;
3342 $logger->info("circulator: processing long-overdue checkin...");
3344 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3345 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3347 $self->lost_bill_options({
3348 circ_lib => $circ_lib,
3349 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3350 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3351 is_longoverdue => 1,
3352 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3353 void_cost_btype => 10,
3354 void_fee_btype => 11
3357 return $self->checkin_handle_lost_or_longoverdue(
3358 circ_lib => $circ_lib,
3359 max_return => $max_return,
3360 ous_immediately_available => 'circ.longoverdue_immediately_available',
3361 ous_use_last_activity =>
3362 'circ.longoverdue.use_last_activity_date_on_return'
3366 # last billing activity is last payment time, last billing time, or the
3367 # circ due date. If the relevant "use last activity" org unit setting is
3368 # false/unset, then last billing activity is always the due date.
3369 sub get_circ_last_billing_activity {
3371 my $circ_lib = shift;
3372 my $setting = shift;
3373 my $date = $self->circ->due_date;
3375 return $date unless $setting and
3376 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3378 my $xact = $self->editor->retrieve_money_billable_transaction([
3380 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3383 if ($xact->summary) {
3384 $date = $xact->summary->last_payment_ts ||
3385 $xact->summary->last_billing_ts ||
3386 $self->circ->due_date;
3393 sub checkin_handle_lost_or_longoverdue {
3394 my ($self, %args) = @_;
3396 my $circ = $self->circ;
3397 my $max_return = $args{max_return};
3398 my $circ_lib = $args{circ_lib};
3403 $self->get_circ_last_billing_activity(
3404 $circ_lib, $args{ous_use_last_activity});
3407 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3408 $tm[5] -= 1 if $tm[5] > 0;
3409 my $due = timelocal(int($tm[1]), int($tm[2]),
3410 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3413 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3415 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3416 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3417 "DUE: $due LAST: $last_chance");
3419 $max_return = 0 if $today < $last_chance;
3425 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3426 "return interval. skipping fine/fee voiding, etc.");
3428 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3430 $logger->info("circulator: check-in of lost/lo item having a balance ".
3431 "of zero, skipping fine/fee voiding and reinstatement.");
3433 } else { # within max-return interval or no interval defined
3435 $logger->info("circulator: check-in of lost/lo item is within the ".
3436 "max return interval (or no interval is defined). Proceeding ".
3437 "with fine/fee voiding, etc.");
3439 $self->needs_lost_bill_handling(1);
3442 if ($circ_lib != $self->circ_lib) {
3443 # if the item is not home, check to see if we want to retain the
3444 # lost/longoverdue status at this point in the process
3446 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3447 $args{ous_immediately_available}, $self->editor) || 0;
3449 if ($immediately_available) {
3450 # item status does not need to be retained, so give it a
3451 # reshelving status as if it were a normal checkin
3452 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3455 $logger->info("circulator: leaving lost/longoverdue copy".
3456 " status in place on checkin");
3459 # lost/longoverdue item is home and processed, treat like a normal
3460 # checkin from this point on
3461 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3467 sub checkin_handle_backdate {
3470 # ------------------------------------------------------------------
3471 # clean up the backdate for date comparison
3472 # XXX We are currently taking the due-time from the original due-date,
3473 # not the input. Do we need to do this? This certainly interferes with
3474 # backdating of hourly checkouts, but that is likely a very rare case.
3475 # ------------------------------------------------------------------
3476 my $bd = cleanse_ISO8601($self->backdate);
3477 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3478 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3479 $new_date->set_hour($original_date->hour());
3480 $new_date->set_minute($original_date->minute());
3481 if ($new_date >= DateTime->now) {
3482 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3485 $bd = cleanse_ISO8601($new_date->datetime());
3488 $self->backdate($bd);
3493 sub check_checkin_copy_status {
3495 my $copy = $self->copy;
3497 my $status = $U->copy_status($copy->status)->id;
3500 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3501 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3502 $status == OILS_COPY_STATUS_IN_PROCESS ||
3503 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3504 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3505 $status == OILS_COPY_STATUS_CATALOGING ||
3506 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3507 $status == OILS_COPY_STATUS_RESHELVING );
3509 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3510 if( $status == OILS_COPY_STATUS_LOST );
3512 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3513 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3515 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3516 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3518 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3519 if( $status == OILS_COPY_STATUS_MISSING );
3521 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3526 # --------------------------------------------------------------------------
3527 # On checkin, we need to return as many relevant objects as we can
3528 # --------------------------------------------------------------------------
3529 sub checkin_flesh_events {
3532 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3533 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3534 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3537 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3540 if($self->hold and !$self->hold->cancel_time) {
3541 $hold = $self->hold;
3542 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3546 # update our copy of the circ object and
3547 # flesh the billing summary data
3549 $self->editor->retrieve_action_circulation([
3553 circ => ['billable_transaction'],
3562 # flesh some patron fields before returning
3564 $self->editor->retrieve_actor_user([
3569 au => ['card', 'billing_address', 'mailing_address']
3576 for my $evt (@{$self->events}) {
3579 $payload->{copy} = $U->unflesh_copy($self->copy);
3580 $payload->{volume} = $self->volume;
3581 $payload->{record} = $record,
3582 $payload->{circ} = $self->circ;
3583 $payload->{transit} = $self->transit;
3584 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3585 $payload->{hold} = $hold;
3586 $payload->{patron} = $self->patron;
3587 $payload->{reservation} = $self->reservation
3588 unless (not $self->reservation or $self->reservation->cancel_time);
3590 $evt->{payload} = $payload;
3595 my( $self, $msg ) = @_;
3596 my $bc = ($self->copy) ? $self->copy->barcode :
3599 my $usr = ($self->patron) ? $self->patron->id : "";
3600 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3601 ", recipient=$usr, copy=$bc");
3607 $self->log_me("do_renew()");
3609 # Make sure there is an open circ to renew
3610 my $usrid = $self->patron->id if $self->patron;
3611 my $circ = $self->editor->search_action_circulation({
3612 target_copy => $self->copy->id,
3613 xact_finish => undef,
3614 checkin_time => undef,
3615 ($usrid ? (usr => $usrid) : ())
3618 return $self->bail_on_events($self->editor->event) unless $circ;
3620 # A user is not allowed to renew another user's items without permission
3621 unless( $circ->usr eq $self->editor->requestor->id ) {
3622 return $self->bail_on_events($self->editor->events)
3623 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3626 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3627 if $circ->renewal_remaining < 1;
3629 # -----------------------------------------------------------------
3631 $self->parent_circ($circ->id);
3632 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3635 # Opac renewal - re-use circ library from original circ (unless told not to)
3636 if($self->opac_renewal) {
3637 unless(defined($opac_renewal_use_circ_lib)) {
3638 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3639 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3640 $opac_renewal_use_circ_lib = 1;
3643 $opac_renewal_use_circ_lib = 0;
3646 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3649 # Desk renewal - re-use circ library from original circ (unless told not to)
3650 if($self->desk_renewal) {
3651 unless(defined($desk_renewal_use_circ_lib)) {
3652 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3653 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3654 $desk_renewal_use_circ_lib = 1;
3657 $desk_renewal_use_circ_lib = 0;
3660 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3663 # Run the fine generator against the old circ
3664 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3665 # a few lines down. Commenting out, for now.
3666 #$self->handle_fines;
3668 $self->run_renew_permit;
3671 $self->do_checkin();
3672 return if $self->bail_out;
3674 unless( $self->permit_override ) {
3676 return if $self->bail_out;
3677 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3678 $self->remove_event('ITEM_NOT_CATALOGED');
3681 $self->override_events;
3682 return if $self->bail_out;
3685 $self->do_checkout();
3690 my( $self, $evt ) = @_;
3691 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3692 $logger->debug("circulator: removing event from list: $evt");
3693 my @events = @{$self->events};
3694 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3699 my( $self, $evt ) = @_;
3700 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3701 return grep { $_->{textcode} eq $evt } @{$self->events};
3705 sub run_renew_permit {
3708 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3709 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3710 $self->editor, $self->copy, $self->editor->requestor, 1
3712 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3715 my $results = $self->run_indb_circ_test;
3716 $self->push_events($self->matrix_test_result_events)
3717 unless $self->circ_test_success;
3721 # XXX: The primary mechanism for storing circ history is now handled
3722 # by tracking real circulation objects instead of bibs in a bucket.
3723 # However, this code is disabled by default and could be useful
3724 # some day, so may as well leave it for now.
3725 sub append_reading_list {
3729 $self->is_checkout and
3735 # verify history is globally enabled and uses the bucket mechanism
3736 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3737 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3739 return undef unless $htype and $htype eq 'bucket';
3741 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3743 # verify the patron wants to retain the hisory
3744 my $setting = $e->search_actor_user_setting(
3745 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3747 unless($setting and $setting->value) {
3752 my $bkt = $e->search_container_copy_bucket(
3753 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3758 # find the next item position
3759 my $last_item = $e->search_container_copy_bucket_item(
3760 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3761 $pos = $last_item->pos + 1 if $last_item;
3764 # create the history bucket if necessary
3765 $bkt = Fieldmapper::container::copy_bucket->new;
3766 $bkt->owner($self->patron->id);
3768 $bkt->btype('circ_history');
3770 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3773 my $item = Fieldmapper::container::copy_bucket_item->new;
3775 $item->bucket($bkt->id);
3776 $item->target_copy($self->copy->id);
3779 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3786 sub make_trigger_events {
3788 return unless $self->circ;
3789 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3790 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3791 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3796 sub checkin_handle_lost_or_lo_now_found {
3797 my ($self, $bill_type, $is_longoverdue) = @_;
3799 # ------------------------------------------------------------------
3800 # remove charge from patron's account if lost item is returned
3801 # ------------------------------------------------------------------
3803 my $bills = $self->editor->search_money_billing(
3805 xact => $self->circ->id,
3810 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3812 $logger->debug("voiding ".scalar(@$bills)." $tag item billings");
3813 for my $bill (@$bills) {
3814 if( !$U->is_true($bill->voided) ) {
3815 $logger->info("$tag item returned - voiding bill ".$bill->id);
3817 $bill->void_time('now');
3818 $bill->voider($self->editor->requestor->id);
3819 my $note = ($bill->note) ? $bill->note . "\n" : '';
3820 $bill->note("${note}System: VOIDED FOR $tag ITEM RETURNED");
3822 $self->bail_on_events($self->editor->event)
3823 unless $self->editor->update_money_billing($bill);
3828 sub checkin_handle_lost_or_lo_now_found_restore_od {
3830 my $circ_lib = shift;
3831 my $is_longoverdue = shift;
3832 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3834 # ------------------------------------------------------------------
3835 # restore those overdue charges voided when item was set to lost
3836 # ------------------------------------------------------------------
3838 my $ods = $self->editor->search_money_billing(
3840 xact => $self->circ->id,
3845 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
3846 for my $bill (@$ods) {
3847 if( $U->is_true($bill->voided) ) {
3848 $logger->info("$tag item returned - restoring overdue ".$bill->id);
3850 $bill->clear_void_time;
3851 $bill->voider($self->editor->requestor->id);
3852 my $note = ($bill->note) ? $bill->note . "\n" : '';
3853 $bill->note("${note}System: $tag RETURNED - OVERDUES REINSTATED");
3855 $self->bail_on_events($self->editor->event)
3856 unless $self->editor->update_money_billing($bill);